Closes the user-flagged scope gap: every previous fluency lane (Phase 5.1 + 5.4-5.7 + grammatical_coverage) operates on 3-word SVO probes. These three pieces stress paragraph-scale generation, give per-stage latency visibility, and expose the realizer's word-choice geometry — all on top of the existing deterministic infrastructure. # discourse_paragraph lane (paragraph-scale fluency) Forces the realizer to emit multi-sentence paragraphs from a multi-step ArticulationTarget with rhetorical moves (ASSERT, SEQUENCE, ELABORATE, CONTRAST). Same realizer, much richer input — every case is 3-5 sentences with deterministic discourse markers. Public 12 cases / holdouts 5 / dev 1 across 12 + 5 topic chains (epistemic, scientific method, creation arc, logical dependency, ethical grounding, linguistic layers, mathematical chain, narrative, biology, physics, two contrast-shaped, musical, social, computational, psychological, economic). Sub-metrics per case: - sentence count (within min..max window) - subject coverage rate - discourse marker presence (next / furthermore / in contrast) - sentence-initial capitalization - replay determinism (run twice, surfaces match) Result: 12/12 public + 5/5 holdouts at 100%, replay rate 100%, mean sentence count 4. # Realizer capitalization (G4, addresses user-flagged concern) generate/realizer.py gains `_capitalize_sentence` + `_join_as_paragraph` helpers. Sentence-initial alphabetic characters are now uppercased (skipping leading whitespace/punctuation). Surfaces went from "wisdom grounds knowledge. next, knowledge requires evidence." to "Wisdom grounds knowledge. Next, knowledge requires evidence." The discourse_paragraph runner ships a strict per-sentence capitalization check so future regressions get caught. # Pipeline-stage profiler (benchmarks/pipeline_profiler.py) External monkey-patch wrapper around CognitiveTurnPipeline.run() that records per-stage ns budgets without editing any pipeline source. Stages: intent, graph_planner, realize_semantic, runtime_chat, maybe_transitive_walk, fold_walk_into_surface, run_teaching, trace_hash. API: `profile_turn(pipeline, text) -> ProfileReport` with `.stages: dict`, `.total_ns: int`, `.as_dict()`. Empirical: runtime_chat dominates >99% on the runtime hot path (which is correct — that's where ingest + propagate + recall + articulate all happen). Future optimisation work has a clear per-stage signal. # Word-selection tracer (benchmarks/word_selection_tracer.py) External wrapper around generate.articulation._resolve_slot that records every nearest-neighbor lookup as a WordSelectionStep: - slot (subject/predicate/object) - input versor (32-d copy) - top-K candidate words by CGA inner product - chosen word + morphology - output language Top-K scoring uses the diagonal Cl(4,1) metric kernel from algebra.backend (same vectorised path vault_recall uses), not a per-word Python loop over cga_inner. No approximation, exact deterministic ranking, bit-identical to a scalar scan. API: `trace_realization(pipeline, text) -> RealizationTrace` with `.steps`, `.realization_steps`, `.surface`, `.as_dict()`. # CLI lane registration Cognition suite now sweeps the benchmark profiler/tracer tests (test_benchmarks_profiler.py) so any future regression in the instrumentation surfaces immediately. # Constraints honoured - Zero edits to core/, chat/, vault/, teaching/, language_packs/, or the algebra hot path. All instrumentation is external monkey-patch with originals restored in finally. - discourse_paragraph runner bypasses ChatRuntime grounding (named v2 gap) so paragraph capability is isolated to the realizer. - No semantic changes; no hidden normalisation; no approximate recall. # Lane health smoke 55, runtime 19, teaching 17, packs 6, cognition 105 (was 103), algebra 132. All Phase 5 fluency lanes still 100% with the capitalised surfaces (rubric is case-insensitive). discourse_paragraph 100%. # What ships next (named v2) - Round-trip: discourse_paragraph through ChatRuntime end-to-end, not just realize_target. - Per-sentence grammatical_coverage rubric on each emitted sentence. - Longer chains (10/20/50 sentences) with per-sentence determinism scaling curves. - compose_relations operator to lift compositionality recall from 68.8% toward 100%.
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_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_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_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)
|