Repository navigation
fix(agentx): serve the replay with its prefix cache, and stop reading an unmeasured round as a result - #1867
lishuoshuo-amd wants to merge 43 commits into
Conversation
…on shared lanes `explore` and `integrate_patch` both need `server_lifecycle` and `benchmark_lane`, and the queue was served oldest-first. On AgentX one serving round takes about an hour, so grids queued earlier held both lanes until FRAMEWORK_AGENT ended and the patches were cancelled unmeasured. Observed on GLM-5.3 MXFP4 (AgentX TP8/EP1/CONC4): a FIFO session queued 16 `integrate_patch` tasks and measured none of them; a session that served patches first measured 7, one of which was a KEEP at +9.5% P50. A reviewed patch holds a worktree and cannot be re-proposed, while a configuration grid can be, so the patch is the one that has to go first. The sort is stable, so FIFO still orders within each group. Admission (budget, lanes, GPU lease) is unchanged, and a running explore is not preempted: a patch takes the lanes when the current holder releases them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`lift_to_current_best` refuses a winner on four paths — not comparable, latency veto, guard refused, no lift — and each only wrote a log line. The lane that measured the winner has already filed its KEEP in its own ledger (`explore_search.accepted`), so a refusal left the two ledgers disagreeing with nothing to reconcile them: the report reads an empty `optimization_stack` and prints a 0% headline for a session that did measure a win. Observed on GLM-5.3 MXFP4 (AgentX, 48h session): explore recorded KEEPs at +48.7% to +51.7%, `optimization_stack` stayed at 0 entries, and the report published `cumulative_gain_validated 0.00%` with verdict REVERT. Nothing in the artifacts said a winner had been refused, or why. Each refusal now lands on `SharedState.unpromoted_keeps` with its action, reason and detail, and the report's completeness annotations render them as a table, so an empty stack states why it is empty. Observable effects: `reports/final.md` gains an "N measured winner(s) were refused promotion" row with a per-refusal table; three of the four refusal paths move from log level info to warning, because each one is a whole measured round being discarded. The refusal paths collapse into one `_hold_current_best` helper. The no-mutation contract in TestWritebackRequiredAxes tightens rather than loosens: a refusal must still leave every promotion field untouched, and must now also leave exactly one audit entry naming the reason. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…d of idling to the budget `kernel_agent` is one lane-holding task that, by design, "runs until the KERNEL phase budget ends it" while holding server_lifecycle, workspace_mutation and benchmark_lane, so no other benchmark shares the GPUs. Every leverage exit in `_kernel_exit` was gated behind `not agent_in_flight`, so once that task was dispatched the only way out was the phase budget — including when the rewrite controller had already reported a terminal status and nothing was pending. Observed on GLM-5.3 MXFP4 (AgentX TP8/EP1/CONC4): the kernel task held every GPU lane for ~195 min with zero kernel_opt attempts and no pending KEEPs, because TraceLens could not split a steady state out of an agentic trace and left no candidates. Orchestration raised it three times and had no mechanism to act on it; the 19 queued explore proposals went unmeasured. The `kernel_controller_done` exit no longer waits for an idle queue. It is the one exit that does not need one: the controller for this macro cycle has reported a terminal status and `kernel_work_pending` is false, so nothing further can come from the in-flight task. The phase transition barrier already stops running actions and waits for the registry to confirm quiet GPUs, so the next phase still starts clean. `kernel_settled` and `kernel_idle_no_progress` stay gated, because both read an empty queue. The exit carries `agent_in_flight` in its evidence, so a transition taken over a running task is legible in the timeline. Observable effect: a forge session whose controller finishes early releases the GPUs to SWEEP instead of holding them to the end of the kernel phase. Geak sessions are unaffected — the exit requires `optimizer == "forge"`. The in-flight contract tightens where it was too broad: test_kernel_agent_in_flight_blocks_every_leverage_exit asserted that an in-flight agent blocks every exit, which is what cost those 195 minutes. It now covers the idle-queue exits only, and a new case pins the controller exit firing while the agent is in flight. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ds start warm Shape-specialised kernels compile on first use, and a serving benchmark meets new shapes for most of a round. Left wherever the image puts them, those caches die with the container, so the first round after a restart pays the compilation and reads as a regression against rounds that did not. Measured on GLM-5.3 MXFP4 (AgentX): one round compiled 238 variants of a single aiter bmm/rope kernel, whose grid constant changes every 64 prefill tokens; of 227 triton variants in a cold run, 59 compiled after the server reported ready and 23 inside the scored window. Each first-use stall freezes the engine for 0.3-0.55 s, and TTFT p90 moved 0.97 s -> 0.55 s between a cold and a warm run of the same configuration. Two real optimizations were rejected twice on cold-run numbers before the cause was found. `FrameworkSpec.jit_cache_envs` names the env vars that relocate a framework's caches; `apply_session_jit_cache` points them at `<session>/runtime/jit-cache/<var>/` on every benchmark subprocess, from both env assembly points (the grid/sweep Magpie run and the baseline/profile shell-out). sglang carries SGLANG_CACHE_DIR and AMD_COMGR_CACHE_DIR -- sglang keeps its shape-specialised triton kernels under SGLANG_CACHE_DIR, not TRITON_CACHE_DIR, which is why setting the triton variable alone would have been inert. Frameworks whose cache location has not been established carry an empty tuple and are untouched. Observable effects: benchmark rounds in one session share a JIT cache, so only the first pays compilation, and `<session>/runtime/jit-cache/` holds it (it grows with the kernels a session compiles). A location the operator set is left alone, and an unwritable directory logs a warning and leaves that cache at its image default rather than failing the round. Scope: this makes rounds within a session comparable. Whether the cache also survives a container restart depends on the session directory's mount, and the official benchmark still starts cold -- prewarming the prefill shapes at server start is a separate lever. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…told to read
`--add-dir` is the only filesystem grant either agent CLI takes, and it does
not separate reads from writes. `_writable_dirs` passed only the worktree and
the workspace, on the reasoning that `integrate_patch` is the sole writer of
framework source and "reads are unaffected" -- but a tree left out of
`--add-dir` cannot be read either, and only one root can seed a worktree, so
the rest were invisible. `SpecialistSubprocessConfig.framework_source_roots`
documented the opposite ("the rest are exposed to the CLI as additional
`--add-dir` entries"); so did the argv test's own comment, next to an
assertion pinning the behaviour that contradicted it.
Measured on GLM-5.3 MXFP4, two sessions on the same model and workload:
- vLLM, installed into `dist-packages`, so no checkout to seed a worktree
from: specialists were handed four source roots and could open none of
them. 25 specialist tasks succeeded and produced zero `integrate_patch`.
Orchestration reported five specialist runs in a row coming back empty.
- sglang, a checkout at `/sgl-workspace/sglang/`: specialists were handed
five roots, one became the worktree, four stayed invisible. All seven
authored patches landed on sglang; none on aiter or sgl_kernel. For
reference, 14 of the 43 patches in the hand-tuned GLM-5.3 stack are
aiter-side.
`_accessible_dirs` now appends every named root that exists, de-duplicated,
after the worktree and workspace. Authoring still belongs in the worktree;
the patch-safety gate, not the filesystem, is what keeps a specialist's
output grounded.
Observable effect: a specialist can read (and, under `bypassPermissions`,
write) the framework trees its task named. The write exposure is new and is
the cost of the grant -- the CLIs offer no read-only `--add-dir`.
Not yet verified end to end: orchestration also reported Bash and grep being
denied on those paths, which points at the outer sandbox as a second gate.
This change is necessary for that path to work and may not be sufficient; a
live session has to confirm a specialist can now grep the aiter tree.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ready measured
A GLM-5.3 AgentX session spent all seven of its `integrate_patch` rounds on
one upstream change (sglang #41259), authored by seven different specialists
across five different gaps, under five different names:
port-sgl-41259-parallel-prompt-encode parallel-prompt-encode-port
parallel_prompt_encode_pr41259 parallel-prompt-encode-pr41259 (x4)
apply-ppe-followups-002
The session's only KEEP (+9.5%) was that patch; the other six rounds
re-measured it, about six hours of the eighteen it had.
Nothing could have caught it. Specialist idempotency keys are built from
where the work came from -- `{batch_id}:{cand_id}`, `{gap_cid}:retry:{n}` --
so five gaps are five legitimately distinct tasks. `research_scout_seen_pr_ids`
exists and is folded into candidate-batch discovery, but it means "read", not
"measured", and that path is not the one these took. The patch name is chosen
by the authoring model, and one change arrived under five of them.
So the identity is read off the diff: the normalised bodies of the lines a
patch adds or removes. Two ports of one upstream change repeat most of those
even when their paths, context and ordering differ; a follow-up to an
already-measured patch does not, which a touched-files identity could not
tell apart. Each settled `integrate_patch` records its set, and a proposal
overlapping one by at least half carries `overlaps_measured` naming the
earlier task, its patch name and outcome.
Reported, never enforced. Only a review that reads both diffs can tell a
duplicate from a follow-up or a corrected retry, and `integrate_patch`
proposals already go to the Critic; a threshold that guessed wrong here
would silently drop real work. If the record shows the judgement is
mechanical, it can become a rule later.
Observable effect: an `integrate_patch` proposal, and the
`specialist_patch_autosubmitted_for_review` observation beside it, now carry
`overlaps_measured` when the session has already spent a round on much the
same diff. Nothing is blocked or re-ordered.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A graded interactivity axis is one percentile of one run's requests, reported as a single number, so two rounds that differ by less than their own request-to-request spread read exactly like two that differ by more. The session KEEPs at +1%. The requests were already on disk and unused. aiperf writes one record per request to `aiperf_artifacts/profile_export.jsonl`, and the graded axes are recoverable from it exactly: on a real GLM-5.3 AgentX run, the median of per-request `e2e_output_token_throughput` over the `profiling` phase reproduces `e2e_norm_intvty_p50` to four decimals (179.6945), and the 10th percentile of the same sample reproduces `e2e_norm_intvty_p90` (101.4887) -- that axis is named for the 90th percentile of latency, which is the 10th of the throughput these records carry. So the reported comparison now carries a bootstrap interval on its percent delta, read from the two runs' exports. Splitting that real run's requests in half and comparing the halves -- one configuration against itself -- gives -0.46% with a 95% interval of [-5.61%, +5.42%]: on 282 requests a side, the request sampling alone is wider than the gate the session decides on. Nothing is gated on it. Thresholds, verdicts and promotion are untouched; the interval is printed under `comparison gain` and says in place that it covers request sampling only, not the run-to-run spread, which on this workload is larger again. It costs no benchmark time: both sides are files the runs already wrote. `SharedState.baseline_workspace` is new, recorded beside `baseline_perf` when the anchor settles, because the snapshot keeps no path back to the requests it was a percentile of. Observable effect: `reports/final.md` gains a `gain CI` line under `comparison gain`, flagged when the interval crosses zero. It is absent when either run's export is gone or has fewer than 30 scored requests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…rent host states The same configuration, measured twice on one GLM-5.3 host, differed by +8.5%. The host sat in two states and the runs were split across them; four KEEPs were withdrawn once the anchor was re-measured. Hyperloom compares a candidate against a stored anchor and cannot re-measure it -- an AgentX round costs an hour -- so the pair spanning two states is invisible. It is not invisible in the requests. A stall lengthens every request in flight, so it lands in the mean long before the median moves: on that host the two states separated cleanly on TTFT mean over median, 1.6-1.7 against 3.4-5.4, with medians within 10% of each other. That ratio needs no second run, only the export each run already wrote. An unrelated session confirms the calibration: a healthy GLM-5.3 AgentX run on a different host fingerprints at 1.639, inside the healthy band measured by hand, and its mean and median reproduce that run's reported aggregates exactly. Against the measured stalled shape the ratio is 2.31; against itself, 1.00. `CROSS_REGIME_RATIO` sits at 1.8, between the spread within one state and the gap between them. The reported comparison now carries both sides' inflation, flagged when the pair spans states. Reported, never enforced: a cross-regime pair is a reason to distrust a number, and only an operator can decide whether to spend an hour re-measuring the anchor. Nothing is gated, re-run or re-ordered. Reading the export is now one function, `profiling_records`, with the interactivity sample and the stall fingerprint both derived from it, because both answer the same question about the same file: how much the reported comparison can carry. Observable effect: `reports/final.md` gains a `stall inflation` line under the comparison, warning when the two runs were not in the same host state. Absent when either run's export is gone or reports fewer than 30 requests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A lane that starts on a share it cannot finish on spends the budget, writes no artifact, and files a failure that reads as if the work were impossible rather than unfunded. Observed on GLM-5.3 MXFP4 (AgentX, 18h): forge-fusion was dispatched at 23:47 against a 31 MB kineto trace with `discover_mode=llm` and `max_turns=100`, three minutes before the session closed, with a timeout of 223 seconds. Its run directory holds `forge_fusion_input.json` and nothing else. The input was never the problem -- the trace was captured, and KernelForge reads it without TraceLens -- the lane was handed the dregs of the phase. Both forge lanes could reach that state, by different routes. Fusion priced a recipe at nothing (`FUSION_MAX_TARGETS if budget_sec > 0 else 0`), so any positive share was fundable and the lane took whatever seconds were left. Gemm priced its targets and correctly found the share unfundable, then passed `lane_budget_sec=0` into a resolver that reads zero as "none could be derived" and falls back to the module default -- running the lane past the phase that could not pay for it, with the tuner ceiling dropped too. So a fusion recipe is now priced, at the sibling lane's per-target figure rather than an invented one: both buy one forge target out of the same phase share, and nothing here has measured them apart. A lane whose derived share pays for no target declines with `lane_underfunded`, naming the share it refused on. `LaneAllocation.is_underfunded` is deliberately not `not is_fundable`: a zero share means no budget could be derived at all -- an unbounded session has no remaining minutes to divide -- which is not the same as a phase with minutes left that cannot pay. Only the latter declines; an unbounded session keeps the module default it had before. Observable effects: a short phase now reports `status=skipped`, `error_class=lane_underfunded` instead of a reaped run with an empty output directory, and the recipe ceiling handed to forge-fuse is the count the share pays for rather than the cap. An operator timeout, or a named tuner, still overrides the decline. The skip is not a failure: nothing was attempted, so no ledger learns the work is impossible. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A triton kernel compiles once per distinct set of specialising constants, and every first compile stalls the whole engine. A constant that tracks a runtime quantity rather than a tuning choice turns that into one compile per call shape, and nothing in the session could say so: the handoff lists trace paths, and a recompile leaves no kernel to rank. Measured on GLM-5.3 MXFP4 (AgentX): one run compiled 238 variants of an aiter bmm/rope kernel whose grid constant moved with the prefill token count, and 59 of that run's 227 triton variants compiled after the server reported ready, 23 of them inside the scored window. TTFT p90 was 0.97 s cold against 0.55 s warm, and two real optimizations were rejected twice on cold numbers before the cause was found by hand. The cache a session fills is now readable -- it lands under `<session>/runtime/jit-cache/` -- so the census is free. Each cache entry is one compiled variant, and its metadata carries the specialising constants appended to the kernel name, which is enough to say where the variants differ without parsing names like `BLOCK_SIZE_N_256` into key and value: the constants appear in the same order in every variant of one kernel, so a positional comparison finds the ones that move. Count alone would be the wrong signal -- a tile-tuned GEMM legitimately carries dozens. What separates them is where they differ: a tuning table spreads across several constants, while a runtime quantity leaks into one and takes a new value per call. A kernel whose widest-spread constant accounts for most of its variants is reported as a leak, and the section says what to do with it -- make that argument runtime rather than `constexpr` -- because the fix changes nothing the kernel computes and no kernel rewrite can recover the stalls. Observable effect: the Forge handoff's `trace-evidence.md` gains a `## Recompiled Kernels` section, so the opportunity agent sees the recompiles alongside the trace paths it already reads. `build_trace_evidence_md` takes the session directory, which it needs to find the cache and which SharedState does not carry. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ey ran in An agentic serving trace has no single steady state, so a ranking over the whole of one is a ranking of whichever phase is largest. On a GLM-5.3 AgentX capture that is prefill by a wide margin: one EXTEND step holds 660 ms of device time against a VERIFY step's 14 ms, and prefill is 1303 ms of the capture's 1417 ms. The two rankings share nothing at the top -- `_sparse_mla_fused_kernel` takes 31.9% of prefill, while decode's leaders are a blockscale GEMM at 18.5%, MoE gemm1 at 13.7% and a reduce-scatter at 10.8% -- so a reader optimising decode off a whole-trace ranking is reading prefill's hotspots. The hand-tuned GLM-5.3 stack says the same thing in patches: `aiter_fmoe_cfg_ep4` and `aiter_fmoe_prefill_ep4` are one operator with two tuned tables, one per phase. The windows were already in the capture. sglang annotates every step with its kind and batch size, and the device timeline carries those annotations beside the kernels, so grouping needs no heuristic: `step[VERIFY bs=3]` names the class, and a class is ranked on its own. Device-side annotations only. The host records the same steps, but its duration is how long submission took -- 0.5 ms against the device's 14.3 ms for one decode step -- and its clock is not the kernels'. A capture with no step annotations ranks nothing rather than falling back to the whole trace, because that fallback is the error this exists to avoid. This is KernelForge reading its own trace, not a TraceLens fallback: `forge-fuse` already takes the raw capture and parses it without TraceLens, and the rewrite controller's opportunity agent is handed the same capture and told to rank measured evidence ahead of inferred. Neither could rank it by phase. The reading primitives move to `kernelforge.trace_events` so the new module does not reach into `fusion.discover` for them. Observable effect: the Forge handoff's `trace-evidence.md` gains a `## Hot Kernels by Step Class` section, one ranking per class, each labelled prefill or decode. Kernel names are cut at 110 characters -- a templated CK symbol runs past 400 of template arguments, and the document is read by an agent on a context budget. The ranking carries name and in-class share only; `bound_type`, `efficiency_percent` and the roofline attributes come from TraceLens and are not reproduced here. It is measured evidence to rank against, not a replacement for that analysis, and it does not enter the kernel-opt ledger. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…old them into A step that runs a split-K GEMM, a few microseconds of reduction, then the one consumer of that reduction pays three launches for one result. Folding the middle one into either side removes a launch without changing what is computed, and that is where a large share of the hand-tuned GLM-5.3 wins came from: the o_proj reduce folded into the pull all-reduce, the q_b reduce into the fused BMM/RoPE, the indexer reduces into the qk kernel. Nothing could point at them. The handoff listed trace paths, and a kernel that costs a launch rather than its work does not rank by GPU time. The signal has to be measured, not named. On a GLM-5.3 decode step `_sparse_mla_reduce_kernel` runs 78 times at 4.2 us, always after `_sparse_mla_split_k_kernel` (389 of 390) and always before one batched GEMM (390 of 390) -- and this repository's kernel categoriser reads that name as attention, so a category filter would have dropped the clearest candidate in the trace. What finds it is the shape of its launches: many per step, microseconds each, the same neighbours every time. The glue ceiling is anchored on the empty-kernel launch floor measured on this hardware, about 1.5 us, so a call within a few multiples of it is mostly launch. At 10 us the census also caught a blockscale GEMM at 9.6 us and a reduce-scatter at 9.9 us, which are the work rather than the glue around it; at 6 us the decode census is the glue alone, and five of its top eight are families the hand-tuned stack patched -- fused quant, AR+RMSNorm, bmm/rope, the split-K reduce, and MoE route-sort-quant. Counted per step class, for the reason the ranking beside it is: a prefill step and a decode step do not run the same glue, and pooling them hides both. Observable effect: the Forge handoff's `trace-evidence.md` gains a `## Launch-Bound Kernels` section naming, per class, what each candidate always sits between -- or that it sits between nothing in particular, which is the case that cannot be folded. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…engine sees it On a cached prefix the GPU does almost no prefill work, so time-to-first- token should barely move with how long the prompt is. When it moves anyway, the cost sits in front of the engine -- rendering the template, re-tokenising the whole prompt, pickling it to the scheduler -- and it is paid per input token whether or not the KV for those tokens is already resident. That cost was the single largest step in the hand-tuned GLM-5.3 stack (+12.7% from encoding the prompt in parallel, +5.5% more from caching tokenised segments), and nothing in a session could see it. Not from the GPU trace, either: in the same run the device was 99.4% busy inside every step and steps covered 98.8% of the capture. There is no idle to find, because the cost is paid before a step exists. It is visible in the requests the run already recorded. On a GLM-5.3 AgentX baseline with a 97.9% prefix-cache hit, TTFT runs 290 ms at 49k of input and 668 ms at 213k -- 230 ms per 100k, against roughly 7 ms per 100k that the hand-tuned stack reached after its host-side work, and within range of the ~180 ms per 100k that stack measured before it. Two sessions, two hosts, the same number. Read as the gap between medians of the two halves of the input range rather than as a fit: TTFT carries a long tail of requests that queued, and a median on each side is not moved by it. A run spanning one prompt length reports nothing rather than a slope over noise. Observable effect: `reports/final.md` gains a `front-end cost` line under the comparison, warned when the growth is large enough to be worth removing. It costs no benchmark time -- the per-request export is a file the run already wrote. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…very kernel A kernel replayed from a captured graph carries no launch call of its own; an eagerly dispatched one does, and the trace records it. Nothing read that line, so a session could not see where the per-launch cost was still being paid. It is rarely uniform. On a GLM-5.3 AgentX capture, target decode runs 99.8% from a graph while prefill and draft extend launch 100% of their kernels individually -- 2479 launches in one prefill step, 82 in one draft extend. Both halves of that split are findings the hand-tuned stack acted on: putting prefill behind a breakable graph was +8.75% on its own, and the draft extend was still eager afterwards because NextN carries no `layers` for the capture to walk. Read from launch correlations rather than from the profiler's dispatch-kind strings, which are ROCm-specific; on this capture the two agree exactly. A capture with no launch calls recorded reports nothing, because reading every kernel as graphed there would be a wrong answer rather than an empty one. Observable effect: the Forge handoff's `trace-evidence.md` gains a `## Graph Coverage` section, one row per step class, flagging a class that pays a launch for every kernel. It says what to establish next -- a module the capture cannot walk is a different problem from a backend that refuses to capture on this platform -- because the fix differs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every signal the gap ledger carried was shaped like an outcome -- short of target, crashed, this action keeps failing, N rounds without a promotion -- which tells a session it is stuck without telling it what it is stuck on. The one cause-shaped source, research hints, reports what others have published rather than what this run is doing. So a session could measure the thing and still not act on it. The censuses landed beside this change read a run's own evidence, but each only rendered into a document for whichever agent happened to read it; nothing turned them into work. Now they do: - a kernel compiled once per call shape, naming the constant that moved - a step class running every kernel eagerly while its siblings replay a graph - time-to-first-token growing per input token on a cached prefix - a launch that always sits beside the same neighbour and could fold into it On GLM-5.3 those four carried the hand-tuned stack's largest single steps -- +12.7% from the prompt path, +8.75% from capturing prefill, and a long run of fused launches -- and not one has an outcome a session would have noticed. Against the real capture they raise six gaps: the recompiled bmm/rope kernel, EXTEND and DRAFT both fully eager, and three launches that each name what to fold them into. Each symptom names what was measured and what it costs, so a specialist is pointed at something to change rather than at a number to improve, and each is keyed on what it is about so repeated refreshes upsert rather than multiply. A per-census cap keeps one pathological run from burying the other signals. `resolve_capture` moves to `kernelforge.trace_events`: both the handoff and this path have to turn a stored trace location, which is sometimes a file and sometimes a rank directory, into the capture to read. Observable effect: `SharedState.gaps` gains rows at layer `run_evidence`, which reach the specialist prompts and the orchestration context the same way every other gap does. Refresh costs one trace parse per census, about 7 s each on a 31 MB capture, on a path that runs a few times per session. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…r reports The underfunded-lane decline I added reported `engine="forge_gemm_tune"`, a name I invented rather than read: that lane's handler tags every other result `engine="forge"`, so the declined rows would have attributed to an engine nothing else writes. `forge_gemm_tune` is also the pre-split module name, which `test_no_stray_standalone_package_references` guards against and which the full suite caught. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…om the timeout they cause When a kernel traps, its rank's queue dies, the next collective never completes, and a watchdog reports a timeout minutes later. That timeout is the last and loudest error in the log, and it points at the collectives, which is the wrong place. On the GLM-5.3 run where this was traced by hand the queue exceptions landed at 19:31 -- eight of them, one per rank -- and the NCCL watchdog fired at 19:41 on an all-reduce that was only waiting for a dead queue. The log carries no memory-access fault, which is what said the kernel trapped rather than ran off its allocation; the cause turned out to be a triton GEMM config returning non-finite values, found by hand with per-layer probes. All a session could say was that the baseline had crashed. Two rules now read the fault itself, and the render says what the pair of them means: a queue exception with a memory fault is a bad address, one without is a device-side assert -- which is what a non-finite value reaching a checked op looks like -- and in both cases a collective timeout afterwards is downstream of the dead queue rather than a communication problem. The patterns are matched against lines a real server.log carries, including the watchdog line, which neither rule claims. Observable effect: `runtime findings` gains `device.queue_exception` and `device.memory_fault`, and a `- reading:` line beside them when a fault was detected. Both apply to every framework: a trapped kernel is a property of the device, not of what dispatched to it. The existing tests that enumerate the rule set carry the two new ids. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Each census test spelled out the same handful of events to ask its own question -- an annotated step on the device timeline, a kernel inside it, the launch call that says the kernel was dispatched rather than replayed -- and two of the files had drifted into carrying 19 duplicated lines, which the metrics gate counts as a new violation. `kernelforge.tests.trace_fixtures` builds them once, with `eager_step` and `graphed_step` for the pair the graph-coverage tests turn on, so each test now opens with the case it is making rather than with the trace format. No behaviour changes; every census test asserts what it did before. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
| "Each first compile stalls the whole engine. A kernel whose variants differ in one constant is " | ||
| "specialising on a runtime quantity: making that argument runtime rather than `constexpr` removes the " | ||
| "recompiles without touching what the kernel computes.", |
| "A class marked ⚠ pays a launch for every kernel it runs. Capturing it costs nothing the kernels " | ||
| "do, and a class that cannot be captured is worth saying why: a module with no `layers` for the " | ||
| "capture to walk is a different problem from one the backend refuses on this platform.", |
# Conflicts: # src/hyperloom/inference_optimizer/tests/test_dispatcher_lane_priority.py # src/hyperloom/orchestrator/loop/writeback.py
… log did not match build_trace_evidence_md enumerates every rule the scan did not match, so adding device.queue_exception and device.memory_fault changed that line and left the hand-off test asserting the old enumeration. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Code metrics gate: PASSED with waived findings (
|
| Dimension | Limit | New | Worse | Baseline out of date | Baseline entries | vs base |
|---|---|---|---|---|---|---|
| Cyclomatic complexity | fails > 20 | 0 | 2 | 0 | 72 | +0 |
| Cognitive complexity | fails > 30 | 0 | 4 | 0 | 284 | +0 |
| Function length (lines) | fails > 80 | 0 | 11 | 0 | 449 | +0 |
| Nested block depth | fails > 5 | 0 | 0 | 0 | 27 | +0 |
| Module length (lines) | warns > 800, fails > 1200 | 0 | 12 | 0 | 53 | +0 |
| Duplicated lines per file | fails > 0 | 0 | 0 | 0 | 118 | +0 |
| Dead code (vulture) | fails > 0 | 0 | 0 | 0 | 2 | +0 |
| Check | Findings | Effect |
|---|---|---|
| Comments | 0 | |
| English only | 0 | |
| Production code imports test code | 0 | |
| Repeated literals | 0 | |
| A1 Private name of another package | 0 | |
| A8 Flag argument | 0 | |
| A9 Tuple return | 0 | |
| A10 Raw vocabulary comparison | 0 | |
| A11 Hidden global write | 0 | |
| A12 Pure forwarder | 0 | |
| A12 Single-implementation abstraction | 0 |
Worse than the baseline (waived by the baseline-raise label): 29
| Unit | Dimension | Baseline | Now |
|---|---|---|---|
src/hyperloom/orchestrator/loop/conversation.py:526 ConversationCollaborator.compose_prompt |
Cognitive complexity | 44 | 46 |
src/hyperloom/orchestrator/loop/intent_router.py:932 IntentRouter.handle_delegate |
Cognitive complexity | 40 | 45 |
src/hyperloom/orchestrator/phases/framework.py:2951 FrameworkPhase.maybe_autosubmit_specialist_patches |
Cognitive complexity | 58 | 59 |
src/hyperloom/orchestrator/phases/framework.py:95 _framework_config_levers_from_done |
Cognitive complexity | 81 | 82 |
src/hyperloom/orchestrator/loop/intent_router.py:932 IntentRouter.handle_delegate |
Cyclomatic complexity | 21 | 24 |
src/hyperloom/orchestrator/phases/framework.py:2951 FrameworkPhase.maybe_autosubmit_specialist_patches |
Cyclomatic complexity | 24 | 25 |
src/hyperloom/orchestrator/actions/executors/_grid_runner.py:628 _run_magpie |
Function length (lines) | 125 | 130 |
src/hyperloom/orchestrator/actions/executors/_workload_envs.py:537 apply_agentx_switch |
Function length (lines) | 104 | 110 |
src/hyperloom/orchestrator/actions/executors/baseline.py:3371 BenchmarkRunExecutor._run_single_benchmark |
Function length (lines) | 578 | 585 |
src/hyperloom/orchestrator/actions/executors/explore.py:758 ExploreExecutor.__call__ |
Function length (lines) | 1124 | 1138 |
src/hyperloom/orchestrator/actions/executors/report.py:484 _build_summary_dict |
Function length (lines) | 86 | 99 |
src/hyperloom/orchestrator/loop/conversation.py:526 ConversationCollaborator.compose_prompt |
Function length (lines) | 119 | 122 |
src/hyperloom/orchestrator/loop/intent_router.py:932 IntentRouter.handle_delegate |
Function length (lines) | 179 | 189 |
src/hyperloom/orchestrator/loop/writeback.py:1854 WritebackCollaborator._promote_baseline |
Function length (lines) | 279 | 281 |
src/hyperloom/orchestrator/phases/framework.py:3126 FrameworkPhase.maybe_autosubmit_framework_config |
Function length (lines) | 138 | 141 |
src/hyperloom/orchestrator/phases/framework.py:2951 FrameworkPhase.maybe_autosubmit_specialist_patches |
Function length (lines) | 170 | 174 |
src/hyperloom/orchestrator/phases/framework.py:95 _framework_config_levers_from_done |
Function length (lines) | 84 | 87 |
src/hyperloom/inference_optimizer/cli/preflight.py:1 |
Module length (lines) | 2785 | 2815 |
src/hyperloom/orchestrator/actions/executors/_grid_runner.py:1 |
Module length (lines) | 2247 | 2254 |
src/hyperloom/orchestrator/actions/executors/_workload_envs.py:1 |
Module length (lines) | 2261 | 2313 |
src/hyperloom/orchestrator/actions/executors/baseline.py:1 |
Module length (lines) | 4253 | 4297 |
src/hyperloom/orchestrator/actions/executors/explore.py:1 |
Module length (lines) | 1880 | 1946 |
src/hyperloom/orchestrator/actions/executors/report.py:1 |
Module length (lines) | 1391 | 1471 |
src/hyperloom/orchestrator/loop/dispatcher.py:1 |
Module length (lines) | 1503 | 1650 |
src/hyperloom/orchestrator/loop/intent_router.py:1 |
Module length (lines) | 1548 | 1680 |
...and 4 more; the job log lists every row.
How to fix
- New or worse: split the unit (extract a function, a class, a module) until it is back under the limit, or no worse than its recorded value. The baseline cannot absorb a new violation: CI refuses any baseline entry added or raised relative to the base branch.
- Baseline out of date: something improved. Run
python scripts/code_metrics.py --update-baselineand commitscripts/code_metrics_baseline.txt; the command only lowers or removes entries. - Override: the
baseline-raisePR label waives new, worse and baseline-growth findings and the failing design checks (they stay listed here). Use it only with the reason in the PR description; it waives nothing else. - Tools for a local run:
pip install ruff==0.16.2 complexipy==8.0.1 vulture==2.16andnpm install -g jscpd@5.4.1(or pointCODE_METRICS_JSCPDat another jscpd command, such asnpx --yes jscpd@<version>).
Module length warning: over 800 lines, within the 1200-line failure limit (not failing): 4
| Module | Lines | Limit |
|---|---|---|
src/hyperloom/inference_optimizer/grid_server_args.py |
983 | warns > 800 |
src/hyperloom/orchestrator/actions/executors/_magpie_patcher.py |
867 | warns > 800 |
src/hyperloom/orchestrator/loop/conversation.py |
984 | warns > 800 |
src/hyperloom/orchestrator/specialists/patch_safety.py |
1122 | warns > 800 |
Already over at the base (backlog, not this change's): 13
| Unit | Dimension | Base | Now |
|---|---|---|---|
src/hyperloom/orchestrator/state/shared_state.py:97 resolve_graded_comparison |
Cognitive complexity | 34 | 34 |
src/hyperloom/orchestrator/actions/executors/explore.py:758 ExploreExecutor.__call__ |
Cognitive complexity | 371 | 371 |
src/hyperloom/orchestrator/loop/writeback.py:3503 WritebackCollaborator.build_env_spec |
Cognitive complexity | 37 | 37 |
src/hyperloom/orchestrator/actions/executors/_workload_envs.py:1380 materialize_config_with_envs |
Cyclomatic complexity | 111 | 111 |
src/hyperloom/orchestrator/actions/executors/_workload_envs.py:383 build_agentx_workload_spec |
Function length (lines) | 118 | 118 |
src/hyperloom/orchestrator/actions/executors/_workload_envs.py:1380 materialize_config_with_envs |
Function length (lines) | 928 | 928 |
src/hyperloom/orchestrator/actions/executors/baseline.py:2309 BenchmarkRunExecutor._run_once |
Function length (lines) | 683 | 683 |
src/hyperloom/orchestrator/actions/executors/explore.py:575 filter_baseline_noop_variants |
Function length (lines) | 112 | 112 |
src/hyperloom/orchestrator/loop/dispatcher.py:801 DispatcherCollaborator.run_task_registered |
Function length (lines) | 111 | 111 |
src/hyperloom/orchestrator/loop/writeback.py:3503 WritebackCollaborator.build_env_spec |
Function length (lines) | 111 | 111 |
src/hyperloom/orchestrator/loop/writeback.py:4541 WritebackCollaborator.validate_geak_via_geak_harness |
Function length (lines) | 187 | 187 |
src/hyperloom/orchestrator/specialists/patch_safety.py:328 resolve_patch_apply_root |
Function length (lines) | 112 | 112 |
src/hyperloom/orchestrator/loop/writeback.py:1 |
Module length (lines) | 4806 | 4805 |
71 findings in files this change does not touch
Outside this change (informational): 71
| Unit | Dimension | Baseline | Now |
|---|---|---|---|
src/hyperloom/orchestrator/enablement/recipe/projections.py project_launch_evidence |
Cognitive complexity | absent | 36 |
src/hyperloom_kb/collect/mapping.py _experience |
Cognitive complexity | absent | 31 |
src/hyperloom_kb/schema.py FieldDeclaration.__post_init__ |
Cognitive complexity | absent | 32 |
src/kernelforge/cli/forge_loop_command.py _prepare_measurement_driver |
Cognitive complexity | absent | 36 |
src/kernelforge/cli/forge_loop_command.py _read_kb_warm_start |
Cognitive complexity | absent | 40 |
src/kernelforge/loop/runner.py IterationLoop._collect_session_candidate |
Cognitive complexity | absent | 44 |
src/kernelforge/loop/runner.py IterationLoop._rule_on_attempt |
Cognitive complexity | absent | 38 |
src/hyperloom_kb/tests/test_cli.py |
Duplicated lines per file | absent | 18 |
src/hyperloom_kb/tests/test_global_identity_and_pull.py |
Duplicated lines per file | absent | 13 |
src/hyperloom_kb/tests/test_local_state.py |
Duplicated lines per file | absent | 18 |
src/hyperloom/inference_optimizer/cli/__init__.py _reexport_session_environment |
Function length (lines) | absent | 112 |
src/hyperloom/inference_optimizer/cli/__init__.py _resolve_agent_lineup |
Function length (lines) | absent | 89 |
src/hyperloom/inference_optimizer/cli/__init__.py _resolve_run_topology |
Function length (lines) | absent | 92 |
src/hyperloom/inference_optimizer/cli/__init__.py _resume_session |
Function length (lines) | absent | 171 |
src/hyperloom/inference_optimizer/cli/__init__.py _seed_fresh_session |
Function length (lines) | absent | 183 |
src/hyperloom/inference_optimizer/multi_node/cli.py _restart_multinode |
Function length (lines) | absent | 107 |
src/hyperloom/inference_optimizer/multi_node/scripts/launch_multinode.py _build_parser |
Function length (lines) | absent | 84 |
src/hyperloom/inference_optimizer/multi_node/scripts/launch_multinode.py _spawn_rank_actors |
Function length (lines) | absent | 90 |
src/hyperloom_kb/http_service.py RequestHandler._dispatch |
Function length (lines) | absent | 91 |
src/hyperloom_kb/sync.py GlobalSync.pull |
Function length (lines) | absent | 86 |
src/kernelforge/cli/forge_loop_command.py _RunReporter.build_result |
Function length (lines) | absent | 91 |
src/kernelforge/cli/forge_loop_command.py _prepare_measurement_driver |
Function length (lines) | absent | 140 |
src/kernelforge/cli/forge_loop_command.py _read_kb_warm_start |
Function length (lines) | absent | 154 |
src/kernelforge/loop/knowledge_integration.py _measure_warmstart_field |
Function length (lines) | absent | 83 |
src/kernelforge/loop/runner.py IterationLoop._close_iteration |
Function length (lines) | absent | 135 |
...and 46 more; the job log lists every row.
Verdict computed by the gate scripts in
/home/runner/work/_temp/code-metrics-gate, not by this tree's copy.
Thresholds and sources: [tool.hyperloom.code_metrics] in pyproject.toml. Tools: complexipy 8.0.1, jscpd 5.4.1, ruff 0.16.2, vulture 2.16.
| "Each first compile stalls the whole engine. A kernel whose variants differ in one constant is " | ||
| "specialising on a runtime quantity: making that argument runtime rather than `constexpr` removes the " | ||
| "recompiles without touching what the kernel computes.", |
| "A class marked ⚠ pays a launch for every kernel it runs. Capturing it costs nothing the kernels " | ||
| "do, and a class that cannot be captured is worth saying why: a module with no `layers` for the " | ||
| "capture to walk is a different problem from one the backend refuses on this platform.", |
… measure The auto-key mixed in the tick, so the same grid proposed on a later tick was a new task. A specialist that keeps re-finding one result -- the agentic session that prompted this queued five grids for the same radix-cache flag under four names -- queued one explore round per round of proposing, each costing a whole benchmark window. An explore grid is now keyed by its variants' canonical fingerprints, so the wording, the ordering and the proposer's own key no longer make a second task of the same measurement. The macro cycle stays in the key, because a later cycle measures against a different stack. The downstream baseline-noop filter only dropped the repeats once something else had made them a no-op, which is exactly what does not happen when the first measurement refuses the configuration. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…s key on A phase asks whether its lane has a gap before it decides it has anything to do, and a dispatcher asks which specialist owns one before it routes it. These rows answered neither: every one carried layer='run_evidence' and a domain of 'kernel' or 'framework', none of which any reader knows. The agentic GLM-5.3 session showed what that costs. KERNEL_AGENT opened, found no gap on its lane -- the recompile and foldable-launch censuses had both raised one -- and escalated straight to CLOSE with "no actionable kernel lever", cancelling its own agent task fifty seconds after enqueueing it, with a fifth of the session's budget unspent. layer now names the lane that can act and domain_hint names a registered specialist; where the evidence came from stays on source and provenance. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A variant that drops a flag and then measures nothing has two readings that lead opposite ways: the flag never came off, or it came off and the hypothesis was wrong. The round recorded only what was asked for, so the two were indistinguishable. On the agentic GLM-5.3 session that cost the run. The radix-cache ablation did reach the server -- the launch carried no --disable-radix-cache and the cache simply never hit on 53k-token prompts -- but the null result read as the removal having been blocked. The session then wrote five source patches to force the flag off, all three that were integrated crashed the server, and a hundred-odd further proposals for the same already-tested configuration crowded out every other direction until the phase closed for want of leverage. A rejected variant now carries whether the flags it ablated are still on the line the server actually ran. An unobserved launch reports nothing rather than claiming either way. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ts round A config lever was read as whatever a proposal appends, and the round was named after proposal_set[0] regardless. Two defects fell out of that. A proposal whose whole change is an ablation -- the way a launcher's baked-in --disable-radix-cache is turned off -- appends nothing, so it read as empty and the reader walked past it. And because the name came from the first proposal while the arguments came from the first non-empty one, a round could launch one proposal's name against another proposal's arguments. The agentic GLM-5.3 session did exactly that six times over. Every integrate_patch it raised carried remove_args=null, so none of them changed the flag they existed to change; one launched radix-cache-on's name with "--speculative-algorithm EAGLE" and no accompanying draft settings, and the engine died loading weights for a speculative head it had not been told how to build. Two more carried no arguments at all and re-measured the baseline. A lever now names its own proposal and carries what it removes and unsets; an ablation-only proposal is a lever; and the known-bad fingerprint reads the ablation too, so two configurations that differ only in what they drop are no longer the same row. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The four censuses ran as one expression, and the caller catches OSError around the whole call. A session whose JIT cache a later process left unreadable therefore lost its launch, graph and front-end findings too -- the trace and the request export were both there and both readable. Replaying yesterday's agentic session off NFS raised exactly that: a PermissionError on runtime/jit-cache, which in a live run would have been swallowed as "no run evidence" with nothing to say which census was missing. Each census is now kept to its own artifact and names itself when it cannot read one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
variant_counts let a PermissionError out of its own is_dir check. Two callers assemble several censuses into one product -- the kernel lane's trace-evidence document and the run-evidence gap set -- so a cache a privileged earlier run left unreadable took the launch, graph and front-end findings down with it. The document one is the worse of the two: it is the only evidence the kernel lane gets, and it would have failed to render at all. Found rendering yesterday's agentic session off NFS, where runtime/jit-cache is root-owned and the replay is not. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An ablation verdict read only the captured launch argv, which SGLang never prints: every SGLang variant that dropped a flag went unjudged, including the one the check exists for. The same launch evidence already carries the engine's own resolved settings, which answer for a flag the engine reports as a boolean. A flag carrying a value falls back to an engine default no record names, and one unanswerable flag leaves the whole verdict unknown rather than reporting over the ones that happened to be judgeable. Observable effect: a variant that asked to remove a server flag now reports whether the flag actually came off the server it measured. Against the GLM-5.3 agentic session's radix-cache round this reads `took_effect: false` --- that server ran with `disable_radix_cache=True` throughout, because the benchmark harness re-injects its own default for any flag the request does not mention. Previously the round reported nothing either way, its null result was read as the hypothesis failing, and the direction was recorded as tested when it had never run. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
AgentX replays agent trajectories: each turn resends the whole conversation so far, so turn i's prompt contains turn i-1's prompt as a prefix. One GLM-5.3 AgentX hour prefilled 44.7M input tokens of which 94.4% were a prefix the same run had already computed --- 2,736 s of per-user prefill time spent recomputing a prefix cache would have served. Two things kept it off. The generic SGLang benchmark scripts inject `--disable-radix-cache`, right for a fixed-sequence throughput sweep and wrong for a replay, and the flag cannot be lifted from the request side: the script re-adds any default the request does not itself name, and naming this one passes it. And Hyperloom never told the harness the run was agentic --- naming the AgentX client script is not that signal --- so the harness read every replay as a throughput sweep. `apply_agentx_switch` now publishes the harness's own `IS_AGENTIC` signal, and `ensure_agentic_prefix_cache` makes the generic scripts honour it as InferenceX's multi-node server script already does. The patch resolves through the copy that actually runs, under `$INFERENCEX_PATH/benchmarks`, and a script whose block has moved is reported rather than silently accepted. It retires once upstream closes the gap. Observable effect: an AgentX round now serves its replay with RadixAttention enabled and no longer applies a context cap. Expect prefill time and TTFT to drop sharply on any agentic workload, and the reported baseline to move with them --- a baseline measured before this change is not comparable to one after. A non-agentic throughput sweep is unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The patch that leaves an agentic replay its prefix cache hung off the grid runner, which baseline and profile do not go through --- they shell out directly. So the one measurement every later round is compared against, and the first server a session ever launches, kept the harness default: a GLM-5.3 AgentX baseline ran with `disable_radix_cache: True` while the session reported the config as agentic. Moved to `prepare_agentx_runtime`, the AgentX preparation step both launch paths already call and which already gates on the AgentX switch, and dropped the grid-runner call so one place owns the decision. Observable effect: an AgentX baseline now serves its replay with RadixAttention enabled, as explore rounds already did. A baseline measured before this is not comparable to one after, and a non-agentic throughput sweep is unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…e wrong GPU install.sh probes torch and refuses to install a CUDA wheel onto a ROCm pod. The probe caught `Exception`, but a module `import torch` pulls in can call `sys.exit` rather than raise --- amd_smi does exactly that when its shared object is not on the loader path --- and SystemExit escaped it. The verdict line never ran, the module's own messages reached the parser in its place, every field defaulted to empty, and the gate reported `torch= is CUDA-built on a ROCm pod` for a pod whose torch was 2.11.0+rocm10.0.0. The probe now catches BaseException, and the shell reads its last line rather than the whole stream, so noise printed ahead of the verdict is not the verdict. Output that still does not parse leaves the match undetermined and says so, instead of being read as a CUDA build. Observable effect: a pod whose torch cannot be imported is now told that, with the import error, instead of being told its torch is built for the wrong GPU. An operator following the old message would reinstall a torch that was already correct, and install.sh would refuse again. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… of KEEP The ledger check in front of a config lever only refused a config that had lost an accuracy gate. A config measured and found not to clear the KEEP bar left no mark, so the same fingerprint could be proposed and measured again in a later cycle --- one agentic GLM-5.3 cycle re-measured a single configuration under a drifting series of names. The bar is `resolve_keep_threshold`, the same decaying per-cycle value every other KEEP decision reads, so a result short of it in an early cycle becomes available again once the bar falls below it, with no second threshold to keep in step. Only a round that reached a measurement settles anything. `integrated` is the one ledger outcome that reports one; a patch that would not apply, a server that failed its smoke and a parity check that could not conclude all record the zero `tps_delta_pct` defaults to. Reading that zero as a result is how a correct direction gets retired on evidence that does not exist: the same session spent three rounds trying to enable the prefix cache, all three died before the server was serving, and the cache was worth 94% of the run's prefill. Observable effect: a configuration whose measurement already fell short of the cycle's KEEP bar is no longer re-dispatched for the same measurement, and the skip names the figure and the bar. A direction whose rounds never produced a measurement is unaffected and stays available. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Six values the branch's new code repeated were literals at each site: the gap severity it raises, the args-mode it defaults to, the baseline workspace a comparison reads against, the places a ratio is reported to, and the microseconds-per-millisecond a serving window converts by. Each name is declared where the value is owned, not beside its use: the severity vocabulary next to the rank that orders it, with the rank derived from the names rather than repeating them; the args-mode pair in the module whose composer defines the two modes. A second copy of a vocabulary is the thing being removed here, so adding one locally would have missed the point. No observable effect: this is a rename, and the code-metrics repeated-literal check goes from six findings to none. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…he patch Grounding ran `git apply --check` and treated every non-zero exit as "valid diff but does not apply to clean base". git exits 1 when it read the patch and the patch does not apply, and 128 when it never got that far because the repository itself was unusable --- a worktree whose parent checkout is gone, a tree owned by another uid. Only the first is about the patch. The specialist worktrees are git worktrees of a checkout inside the pod, so anything reading them after that pod is gone --- a later leg, an archived session, an operator on the login node --- hits 128 and labelled every patch in the set stale. That label reaches `patch_grounding`, the `patch_safety_stale` audit note, and from there whoever decides whether the round is worth running. Observable effect: a patch whose repository could not be examined is now recorded as `unchecked` with git's own message, not as a stale patch. A patch git did examine and reject still reads `stale`, unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two conflicts, both where main had rebuilt the mechanism this branch was changing. `patch_safety`: main moved the git call into `_git_apply_check`, which already returns `None` for "git could not run". The branch's fix --- git exits 128 for a fatal error about the repository and 1 for a patch that does not apply, and only the second is a verdict about the patch --- belongs inside that helper, so it was carried there rather than re-applied at the call site. The call site's `git unavailable:` prefix went with it: the verdict is already named `unchecked`, and the prefix is untrue of a repository git could not read. `request_handlers`: main replaced this branch's `is_underfunded` / `_lane_underfunded` with `priced_gemm_lane` returning `None` and `_unfunded_lane_skip`, under the heading that phase budgets are the operator's. The branch's version carried an operator override --- a `timeout_sec` or a named `tuner` ran the lane anyway --- which is the behaviour main set out to remove, so the whole mechanism is dropped and its test contracts re-pinned on main's path. Kept from this branch: pricing a fusion recipe. main counts a fusion share fundable whenever it is non-zero, so the skip its own change describes cannot fire for that lane --- a 223-second share still reports it can fund recipes. Priced per recipe, a share too small for one reports no fundable target, which is what `_unfunded_lane_skip` reads. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…dy holds The phase block reports how much budget is left but not what a round costs, and the admission gate prices one action against the phase clock without subtracting the work already committed to it. Both readings say "affordable" while the queue is oversubscribed. A FRAMEWORK_AGENT phase observed on 2026-10-15 proposed thirteen explore rounds over 39 minutes and measured four. By the fifth proposal the queue already held more minutes of work than the phase had left, yet each round priced on its own still fitted; the remaining nine waited and were dropped in one batch when the budget ran out, each already reasoned about and reviewed. The orchestration prompt now carries an `=== Affordable now ===` line marking each of the phase's actions the budget cannot fund once the queue's own minutes are charged against it, and names the committed total. The gate grew an `already_committed_minutes` argument for this; it stays zero at dispatch, where the queue is being drained rather than added to, so admission is unchanged. Observable effect: the orchestration is told, before it proposes, which of its actions this phase can still pay for given what it has already asked for. A round it would have proposed and had dropped is one it can now decline to propose. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
b9f5efb to
56518e9
Compare
… round Admission priced one action against the phase clock and ignored the work already queued against it, so every round in an oversubscribed queue still fitted. Two observed FRAMEWORK_AGENT phases admitted thirteen and fourteen explore rounds and measured two and one; the rest were cancelled when the phase ended, each already reasoned about, reviewed and recorded first. The prompt-side note added earlier reported the committed total but changed nothing, because it only informed the proposer. `admission_denial_for_action` now charges `queued_cost_minutes()` alongside the action's own cost. It is the single entry point the intent handlers share, so every delegate path is covered at once, and the refusal travels the existing `record_policy_denied` route — the orchestration is told which budget refused it and can propose something the phase can pay for. The phase and sequence gates run first and are unchanged. Observable effect: a round the phase cannot pay for once its queue is counted is refused when proposed, instead of being created, reviewed, queued, and cancelled unmeasured at the phase boundary. Sessions will show fewer queued tasks and fewer cancellations, not fewer measurements. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The queued-cost charge summed every queued task, so a research_lane specialist priced as if it had to finish before a server_lifecycle explore round could start. A phase holding two specialists would refuse an explore round it can in fact measure alongside them -- a worse waste than the one the charge exists to prevent. The charge is now taken over the queued tasks whose required lanes intersect the admitted action's. An action with no lanes on record charges the whole queue, as before. Making admission_denial_for_action async left the loop's own phase admission tests calling it bare; they now drive it through asyncio.run and carry a queue the fake coordinator can answer. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…missed Making admission_denial_for_action a coroutine left four callers on the synchronous shape. One was repaired with the change; a tree-wide grep on the symbol finds the other three, which the CI log's per-shard view does not make obvious: two stub it with a plain lambda an await then rejects, one calls it bare and asserts on the coroutine. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
PR #1867 -- fix(agentx): serve the replay with its prefix cache, and stop reading an unmeasured round as a resultWhat it does: an AgentX replay was being served as if it were a fixed-sequence throughput sweep. Blocking issues: 5
deferred: free:baseexception -- src/hyperloom/inference_optimizer/assets/install.sh:866 adds SKIPPED: V4 -- openprs.txt carries "#1032 changes 1075 files, only its first 100 were compared" and the same for #1154 (1232), #1276 (1311), #1675 (161) and #1869 (315); overlap with those five is unchecked, not absent. Checked: dispatcher.py, conversation.py, intent_router.py, writeback.py, shared_state.py, phases/framework.py, phases/machine_state.py, explore.py, _workload_envs.py, _magpie_patcher.py, patch_safety.py, install.sh, action_surfaces.py, agents/framework/kb.py, knowledge/kb_writeback.py; tiers for all 35 changed non-test src files; tree-wide sweeps for every non-test writer and reader of baseline_workspace, unpromoted_keeps, record_unpromoted_keep, patch_grounding and the eight new modules; |
…tack The config-lever ledger is one lessons.jsonl under the workspace root, shared by every session that root has served, and the new throughput branch matched a record on its config fingerprint alone. A lever worth +0.4% against a 2% bar on Qwen3-8B/MI300X therefore skipped that direction, unmeasured, for a later GLM-5.3/MI355X session -- the same failure the branch exists to prevent, one level up. The record already carries model_class, gpu_type and precision, written from the same shared_state fields a session reads; the throughput branch now requires all three to match. The accuracy gate above it keeps matching on the fingerprint alone: an accuracy regression travels with the config, a throughput number travels with the stack. Observable effect: a config lever a different model or GPU measured short of KEEP is now proposed and measured rather than silently skipped. The skip log is unchanged for a result from the running stack. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
admission_frozen is already set for as long as a transition is pending, and its comment says "Admission stays frozen" -- but only the dispatch pump read it. The gate that decides whether a proposed action may be enqueued did not, so during a pending exit the orchestration proposed, admission answered allowed=true denial_rule=null, the task was queued, the pump refused to dispatch it, and the same tick's drain cancelled it. One observed FRAMEWORK_AGENT phase spent seventeen minutes that way. The transition to KERNEL_AGENT was decided at 20:52 but held on one running explore, and from 20:53 to 21:09 eleven rounds were reasoned about, reviewed, admitted and cancelled within half a second each, every one under phase_transition:FRAMEWORK_AGENT->KERNEL_AGENT. None of them left a denial for the orchestration to learn from. The freeze now answers first, ahead of the phase and budget gates: a phase on its way out is not a budget question, so the clock is not read to say no. The denial carries a hint to propose again after the next phase is entered, because a bare refusal reads as "never". Observable effect: during a pending phase transition a proposed action is refused with rule=phase_transition_pending instead of being queued and cancelled unmeasured. An operator previously saw a run of phase_transition cancellations with no denial explaining them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…dent A `git add -A` swept the intent-envelope fix and its test into the admission commit, where it has nothing to do with the phase freeze and duplicates PR #1923, which owns that change against main. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The missing-CLI warning told the operator to run install.sh, and the headers of install.sh and install_kernel_tools.sh both claimed step 4 provisions the Claude CLI. Neither script installs node, npm or either CLI; grep for npm finds only assets/slurm/_incontainer.sh.in, which states the real contract where it refuses to start without them: "install.sh does not provide node/npm, so the image must". Naming the wrong remedy cost a GLM-5.3 run on a plain docker image. Preflight warned, install.sh was rerun, nothing changed, ClaudeBackend fell back to direct HTTP, and the retries stretched each tick until a proposal sat 3443s against a 1800s review TTL -- a TTL enforced at the top of a tick bounds nothing once a tick runs longer than it. Observable effect: the warning now says the CLIs come from the container image and that the fallback is slower and retries more, instead of pointing at a script that cannot supply them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The explore idempotency key is a digest of a whole grid, so {A,B} and
{A,C} are two keys and A is measured twice. Deduplicating the grid
admits one configuration once per grouping it is proposed in, and the
orchestration proposes the same lever in grouping after grouping.
Measured on a live FRAMEWORK_AGENT queue: 34 variants under 21 distinct
configurations -- 13 exact duplicates, one lever queued five times. Each
copy is a server launch and a benchmark holding the single serving lane,
roughly 52 minutes of a 274-minute phase, while the cycle's two reviewed
patches waited behind them for a lane that never freed.
The variant is what costs the lane, so the variant is what has to be
unique at enqueue. A proposed grid is now narrowed against what the
queue and the running round already hold, and a grid left with nothing
is refused with an observation naming how many were dropped, rather than
queued as a round that measures nothing. An explore delegated with no
grid is untouched: the executor seeds its own.
ConversationCollaborator asked the queue the same question to build the
prompt; it now asks the dispatcher, so the line the agent reads and the
gate that admits are one set rather than two that can drift.
Observable effect: an explore proposing a configuration already queued
is admitted with that variant removed, and the orchestration is told
which. Previously every copy ran.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
What this changes
A GLM-5.3 AgentX session is driven by the Coordinator across FRAMEWORK and
KERNEL. Two runs of it produced one KEEP between them, no landed source patch,
and a KERNEL phase that closed a minute after it opened. This branch is what
each of those turned out to be.
An agentic replay is served with its prefix cache on
AgentX replays agent trajectories: each turn resends the whole conversation so
far, so turn i's prompt contains turn i-1's prompt as a prefix. One hour of
the GLM-5.3 workload prefilled 44.7M input tokens, of which 94.4% were a
prefix the same run had already computed — 2,736 s of per-user prefill spent
recomputing what a cache would have served.
Two things kept it off. The generic SGLang scripts inject
--disable-radix-cache, and the flag cannot be lifted from the request side:the script re-adds any default the request does not itself name, and naming
this one passes it. And Hyperloom never told the harness the run was agentic —
naming the AgentX client script is not that signal — so every replay was served
as a fixed-sequence throughput sweep.
apply_agentx_switchnow publishes the harness's ownIS_AGENTIC, andensure_agentic_prefix_cachemakes the generic scripts honour it asInferenceX's multi-node server script already does. It is called from
prepare_agentx_runtime, the one AgentX preparation step every launch pathreaches: hung off the grid runner it never reached a baseline, which is the
measurement every later round is compared against.
Verified on hardware: a baseline launched after this reports
'disable_radix_cache': False, againstTruebefore it.A round that could not measure is no longer read as a result
Three defects, one shape — an undetermined check reported as a negative
verdict:
never prints, so every SGLang ablation went unjudged. It now asks the
engine's own resolved settings, and leaves a flag it cannot judge unknown.
git apply --checkexits 1 when the patch does notapply and 128 when the repository itself was unusable. Every non-zero was
recorded
stale, so a worktree whose pod is gone labelled every patch in theset broken.
Exception, but a moduleimport torchpulls in can callsys.exit; the verdict line never ran, andthe gate reported
torch= is CUDA-built on a ROCm podfor a pod whose torchwas
2.11.0+rocm10.0.0.The lever a specialist proposed reaches the round that measures it
_framework_config_levers_from_doneinspected onlyextra_args/extra_envs,so an ablation-only proposal looked empty and was skipped, and the patch name
came from
proposals[0]while the args came from another. One round launched abare
--speculative-algorithm EAGLEand died in the weight loader.Work is not queued, or measured, twice
the tick. Replayed against the observed session: 120 tasks collapse to 26
distinct measurements, one configuration having been proposed 61 times under
drifting names.
re-dispatched. Only
integratedsettles anything — a patch that would notapply or a server that failed its smoke records the zero
tps_delta_pctdefaults to, and reading that as a result retires a correct direction on
evidence that does not exist.
KERNEL_AGENT is given a lever shaped like a cause
Run-evidence gaps now carry the lane and domain their readers key on, each
census survives a sibling's unreadable artifact, and the hand-off names what a
trace actually shows. Rendered from the observed session's artifacts, that
document reports EXTEND bs=1 prefill spending 97.0% / 31,453 ms in
quickreduce::allreduce_prototype_twoshot.Observable effect
cap. A baseline measured before this is not comparable to one after; a
non-agentic throughput sweep is unchanged.
off the server it measured.
unchecked, notstale.precision is not measured again, and the skip names the figure and the bar.
A result from another stack in the same workspace no longer skips it: the
ledger is one file per workspace root and a throughput delta is a property of
the stack, not of the config.
committed on the lanes it needs is refused when proposed, rather than queued
and cancelled unmeasured at the phase transition.
unpromoted_keeps, and that refusal is logged at WARNING rather than INFO --the lane that measured the winner has already filed its KEEP elsewhere, so a
quiet log left the session reporting 0% for work it did do.
each round rebuilding.
phase budget.
Tests
pyteston the touched trees; ~1,900 selected tests pass. New tests pin eachfix against the failure it came from, including the two that were verified to
fail without the fix (the torch gate's misdiagnosis and the 128 grounding
verdict).
Telling the orchestration was not enough, so admission now refuses
feat(prompt): price this phase's actions against what the queue already holdswas written against an observed waste: a FRAMEWORK_AGENT phase proposed
thirteen explore rounds, measured four, and dropped nine. Run again with the
change in place, the next session proposed and then cancelled ten. Telling
the orchestration what the queue has already spoken for did not change how much
it proposes.
fix(admission): charge the queue's committed minutes when admitting a roundis the follow-on:
admission_denial_for_actionnow prices a proposed roundagainst the minutes the queue has already committed on the lanes that round
needs, not against the bare phase clock. A later commit corrects the charge to
be lane-aware — two queued specialists on
research_laneno longer refuse anexplore round the phase can measure alongside them.
Observable effect: a round the phase cannot pay for alongside the work
already queued is refused when it is proposed, with
action='explore' denied: needs ~12 min but only N min of the FRAMEWORK_AGENT phase budget is left, instead of queueing and being cancelled unmeasured whenthe phase ends. An operator previously saw a long queue drain into
phase_transitioncancellations with no explanation of what the phase hadalready spent.
The remaining cause is not addressed here: the orchestration proposes
permutations of one direction, each with its own fingerprint, so neither the
idempotency key nor the budget note stops it proposing them. Giving the
reviewer the in-flight directions belongs in its own PR.
Code metrics:
baseline-raiserequestedNew violations: 0 in every dimension; all design checks report 0; repeated
literals went 6 → 0.
25 entries are worse than baseline, every one of them growth of a unit that
was already over its limit before this branch —
explore.py,framework.py,baseline.py,intent_router.pyand their modules. No unit crosses athreshold it was under, and nothing here adds a branch or a second
responsibility to an over-limit unit: the additions are assignments, one call,
and one new module-level helper. Splitting those units is a separate change
against a quieter diff.
🤖 Generated with Claude Code