Bundle of 5 hot-path optimizations + 1 dead-code removal + 1 import
sweep + 1 helper fold, surfaced by a comb pass through the cognitive
spine starting from ``CognitiveTurnPipeline.run()`` and walking
outward through ChatRuntime, intent classification, the graph
planner, the realizer, and the vault. All eval lanes byte-identical
to MEMORY baseline; null-lift confirmed by ``core eval cognition``
across public / dev / holdout splits.
Hot-path fixes:
1. ``ChatRuntime._apply_oov_policy`` no longer rescans every
manifest per OOV token. Two precomputed booleans on
``self`` capture the FAIL_CLOSED-all and PROPOSE_VOCAB-any
aggregates at construction time. Manifests are immutable
post-construction so the cache is safe. Turns the path from
O(packs × OOV) to O(OOV).
2. ``CognitiveTurnPipeline.run`` calls ``classify_compound_intent``
once and takes its dominant ``compound.primary`` as the seeded
intent. Pre-fix the pipeline called both ``classify_intent``
and ``classify_compound_intent`` on every turn — and
``classify_compound_intent`` internally invokes
``classify_intent`` on the dominant fragment, so every non-
compound prompt walked the 15-regex cascade twice.
3. ``TeachingStore.triples()`` materializes once per turn.
Pre-fix ``_maybe_transitive_walk`` and ``_maybe_compose_relations``
each called ``self.teaching_store.triples()`` independently,
doubling the per-turn O(N) filter+tuple-build cost. Both
helpers now accept an optional ``triples`` arg; the pipeline
computes once and passes through.
5. ``realize_semantic`` and ``realize_target`` build a
``node_id → obj`` map once and look up each step in O(1)
instead of an O(N) linear scan of ``graph.nodes`` per step.
The cost was invisible on today's 1-2 node graphs but would
have become an O(N²) regression on the multi-node graphs
ADR-0089 Phase C2 plans to introduce.
Dead-code / cleanup:
- Removed dead ``CognitiveTurnPipeline._fold_compose_into_surface``
(no callers since PR #76 routed all surface composition
through ``resolve_surface``).
- Folded ``_serialize_walk`` + ``_serialize_compose`` (identical
bodies) into one ``_serialize_operator`` helper.
- Hoisted ``import json`` and ``RatifiedIntent`` from inside hot
method bodies to module top (same pattern PR #76 applied to
``_is_useful_surface``).
- Dead-defensiveness sweep on ``ChatResponse`` field reads in
``pipeline.run()``: ``getattr(response, "<field>", default)``
where the field always exists on the dataclass with a default
is replaced by direct attribute access (6 sites:
``realizer_grounded_authority``, ``recalled_words``,
``grounding_source``, ``register_canonical_surface``,
``pre_decoration_surface``, ``admissibility_trace``,
``region_was_unconstrained``). ``refusal_reason`` retains the
guarded read because ADR-0024 Phase 2 leaves its
materialisation site dormant.
Benchmark profiler:
- ``benchmarks/pipeline_profiler.py`` rebound from
``classify_intent`` to ``classify_compound_intent`` (the new
single-classification site). All other timing hooks unchanged.
Tests:
- 4 new tests in ``tests/test_comb_pass_hot_path.py`` pin: OOV
aggregates exist as bools; compound classifier runs exactly
once per turn; ``triples()`` materializes exactly once per
turn; realizer correctly resolves obj slots across an 8-node
graph.
- All existing tests pass. ``core eval cognition`` byte-identical:
public 100/100/91.7/100, dev 100/100/78.6/100, holdout
100/100/83.3/100.
- ``core test --suite cognition`` 120/0/1, ``smoke`` 67/0,
``runtime`` 19/0.
182 lines
7 KiB
Python
182 lines
7 KiB
Python
"""Pipeline-stage profiler for CognitiveTurnPipeline.
|
|
|
|
External instrumentation only — no edits to pipeline/runtime/algebra/vault
|
|
source files. Uses lightweight monkey-patching of bound methods on the
|
|
pipeline instance and the runtime instance for the duration of a single
|
|
``profile_turn`` call. All patches are reverted in a ``finally`` block so
|
|
the pipeline is left untouched.
|
|
|
|
Per CLAUDE.md: no hidden normalization, no semantic mutation, no algebra
|
|
hot-path touch. Overhead per stage: a single ``time.perf_counter_ns``
|
|
read on entry and on exit, and a list append. Stage label strings are
|
|
pre-interned at module load time (no f-strings inside timed regions).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import time
|
|
from contextlib import contextmanager
|
|
from dataclasses import dataclass, field
|
|
from typing import Any, Iterator
|
|
|
|
from core.cognition.pipeline import CognitiveTurnPipeline
|
|
from core.cognition.result import CognitiveTurnResult
|
|
|
|
|
|
# Pre-interned stage label constants — avoid string construction in
|
|
# the timed hot path.
|
|
_STAGE_INTENT = "intent"
|
|
_STAGE_GRAPH = "graph_planner"
|
|
_STAGE_REALIZE = "realize_semantic"
|
|
_STAGE_RUNTIME_CHAT = "runtime_chat"
|
|
_STAGE_TRANSITIVE_WALK = "maybe_transitive_walk"
|
|
_STAGE_FOLD_WALK = "fold_walk_into_surface"
|
|
_STAGE_TEACHING = "run_teaching"
|
|
_STAGE_TRACE = "trace_hash"
|
|
_STAGE_TOTAL = "total"
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ProfileReport:
|
|
"""Immutable timing report for a single profiled turn."""
|
|
|
|
stages: dict[str, int]
|
|
total_ns: int
|
|
result: CognitiveTurnResult
|
|
|
|
def as_dict(self) -> dict[str, Any]:
|
|
return {
|
|
"stages": dict(self.stages),
|
|
"total_ns": int(self.total_ns),
|
|
}
|
|
|
|
|
|
@dataclass
|
|
class _ProfileSink:
|
|
"""Mutable per-call accumulator. Not shared across calls — instantiated
|
|
fresh in every ``profile_turn`` invocation, so no global state."""
|
|
|
|
stages: dict[str, int] = field(default_factory=dict)
|
|
|
|
def record(self, name: str, elapsed_ns: int) -> None:
|
|
# Multiple invocations of the same stage in a turn are summed.
|
|
prior = self.stages.get(name, 0)
|
|
self.stages[name] = prior + elapsed_ns
|
|
|
|
|
|
@contextmanager
|
|
def _stage(sink: _ProfileSink, name: str) -> Iterator[None]:
|
|
"""Lightweight context manager: two perf_counter_ns reads plus a dict update."""
|
|
t0 = time.perf_counter_ns()
|
|
try:
|
|
yield
|
|
finally:
|
|
sink.record(name, time.perf_counter_ns() - t0)
|
|
|
|
|
|
def profile_turn(
|
|
pipeline: CognitiveTurnPipeline,
|
|
text: str,
|
|
max_tokens: int | None = None,
|
|
) -> ProfileReport:
|
|
"""Profile one CognitiveTurnPipeline.run() invocation.
|
|
|
|
Wraps the pipeline's existing internal methods and the runtime's
|
|
``chat`` method with timing decorators for the duration of this call,
|
|
then restores them. Patches live on the *instances*, not on the
|
|
classes, so concurrent profiling of distinct pipeline instances is
|
|
safe.
|
|
"""
|
|
sink = _ProfileSink()
|
|
|
|
# Capture originals (instance attrs win over class attrs in resolution,
|
|
# so reassigning attrs on the instance does not mutate the class).
|
|
runtime = pipeline.runtime
|
|
orig_chat = runtime.chat
|
|
orig_maybe_walk = pipeline._maybe_transitive_walk
|
|
orig_fold = pipeline._fold_walk_into_surface
|
|
orig_run_teaching = pipeline._run_teaching
|
|
|
|
# We patch generate.intent / graph_planner / realizer via per-call
|
|
# module-attribute swaps on the pipeline module so we only time the
|
|
# functions actually called from pipeline.run().
|
|
from core.cognition import pipeline as pipeline_mod
|
|
|
|
orig_classify_intent = pipeline_mod.classify_compound_intent
|
|
orig_graph_from_intent = pipeline_mod.graph_from_intent
|
|
orig_plan_articulation = pipeline_mod.plan_articulation
|
|
orig_realize_semantic = pipeline_mod.realize_semantic
|
|
orig_compute_trace_hash = pipeline_mod.compute_trace_hash
|
|
|
|
def timed_classify_intent(*args: Any, **kwargs: Any) -> Any:
|
|
with _stage(sink, _STAGE_INTENT):
|
|
return orig_classify_intent(*args, **kwargs)
|
|
|
|
def timed_graph_from_intent(*args: Any, **kwargs: Any) -> Any:
|
|
with _stage(sink, _STAGE_GRAPH):
|
|
return orig_graph_from_intent(*args, **kwargs)
|
|
|
|
def timed_plan_articulation(*args: Any, **kwargs: Any) -> Any:
|
|
with _stage(sink, _STAGE_GRAPH):
|
|
return orig_plan_articulation(*args, **kwargs)
|
|
|
|
def timed_realize_semantic(*args: Any, **kwargs: Any) -> Any:
|
|
with _stage(sink, _STAGE_REALIZE):
|
|
return orig_realize_semantic(*args, **kwargs)
|
|
|
|
def timed_compute_trace_hash(*args: Any, **kwargs: Any) -> Any:
|
|
with _stage(sink, _STAGE_TRACE):
|
|
return orig_compute_trace_hash(*args, **kwargs)
|
|
|
|
def timed_chat(*args: Any, **kwargs: Any) -> Any:
|
|
with _stage(sink, _STAGE_RUNTIME_CHAT):
|
|
return orig_chat(*args, **kwargs)
|
|
|
|
def timed_maybe_walk(*args: Any, **kwargs: Any) -> Any:
|
|
with _stage(sink, _STAGE_TRANSITIVE_WALK):
|
|
return orig_maybe_walk(*args, **kwargs)
|
|
|
|
def timed_fold(*args: Any, **kwargs: Any) -> Any:
|
|
with _stage(sink, _STAGE_FOLD_WALK):
|
|
return orig_fold(*args, **kwargs)
|
|
|
|
def timed_run_teaching(*args: Any, **kwargs: Any) -> Any:
|
|
with _stage(sink, _STAGE_TEACHING):
|
|
return orig_run_teaching(*args, **kwargs)
|
|
|
|
pipeline_mod.classify_compound_intent = timed_classify_intent
|
|
pipeline_mod.graph_from_intent = timed_graph_from_intent
|
|
pipeline_mod.plan_articulation = timed_plan_articulation
|
|
pipeline_mod.realize_semantic = timed_realize_semantic
|
|
pipeline_mod.compute_trace_hash = timed_compute_trace_hash
|
|
runtime.chat = timed_chat # type: ignore[assignment]
|
|
pipeline._maybe_transitive_walk = timed_maybe_walk # type: ignore[assignment]
|
|
pipeline._fold_walk_into_surface = timed_fold # type: ignore[assignment]
|
|
pipeline._run_teaching = timed_run_teaching # type: ignore[assignment]
|
|
|
|
t_total_0 = time.perf_counter_ns()
|
|
try:
|
|
result = pipeline.run(text, max_tokens=max_tokens)
|
|
finally:
|
|
total_ns = time.perf_counter_ns() - t_total_0
|
|
# Restore originals (instance and module).
|
|
pipeline_mod.classify_compound_intent = orig_classify_intent
|
|
pipeline_mod.graph_from_intent = orig_graph_from_intent
|
|
pipeline_mod.plan_articulation = orig_plan_articulation
|
|
pipeline_mod.realize_semantic = orig_realize_semantic
|
|
pipeline_mod.compute_trace_hash = orig_compute_trace_hash
|
|
runtime.chat = orig_chat # type: ignore[assignment]
|
|
try:
|
|
del pipeline._maybe_transitive_walk # restore class-bound method
|
|
except AttributeError:
|
|
pipeline._maybe_transitive_walk = orig_maybe_walk # type: ignore[assignment]
|
|
try:
|
|
del pipeline._fold_walk_into_surface
|
|
except AttributeError:
|
|
pipeline._fold_walk_into_surface = orig_fold # type: ignore[assignment]
|
|
try:
|
|
del pipeline._run_teaching
|
|
except AttributeError:
|
|
pipeline._run_teaching = orig_run_teaching # type: ignore[assignment]
|
|
|
|
return ProfileReport(stages=dict(sink.stages), total_ns=total_ns, result=result)
|