Timing model hardening: overlay/execution drift, picosecond stage accounting, classification drift detection - #99
Merged
Conversation
…es properly
Two defects in the experimental prefetch/compute overlap estimator, both from
`timing_access_for_opcode` hand-mirroring the SRAM addressing of the execution
arms in `do_ops` with no shared source of truth.
Divergence that had already happened: `do_ops` has zero-time no-op arms
(`V_RED_SUM { rd: 0 }` / `V_RED_MAX { rd: 0 }` discard their fp0 write-back and
return without touching vram), but the overlay classified those opcodes by shape
alone and reported a read range. `record` then ran `retire_dependent_prefetches`
unconditionally, so a no-op could retire a prefetch it never read — permanently
losing the chance to hide it and inflating `dependent_prefetch_stalls` /
`compute_ops`. Effect was conservative, not wrong-in-the-fast-direction, but it
is exactly the drift this mirroring invites.
Rather than mirror each no-op guard (which would need re-syncing forever), gate
on observed elapsed time: an opcode that consumed no simulator time performed no
SRAM access, because every real read awaits mram/vram and advances the clock.
This is the same guard the Prefetch arm already had, and it covers no-op arms
added later for free.
`H_STORE_V` mapped to `Barrier`, which abandoned *every* pending prefetch. It is
really an SRAM read (`src_addr = gp(rd)`, extent `VLEN * STORE_V_AMOUNT`) feeding
an HBM write, so any program interleaving stores got essentially no overlap
credit. Added a `Store` access kind that retires the prefetches it genuinely
depends on but does not hide pending prefetch time — store and prefetch share the
DMA path, and the model does not track bandwidth. `C_BREAK` stays a real barrier.
Also removed the `_ => TimingAccess::Other` wildcard so the match is exhaustive:
a newly added opcode now fails to compile until it is classified, instead of
being silently absorbed into "no SRAM access". Neither guard catches a *wrong*
address, so the mirroring invariant is now documented on the function, and the
module docs state plainly that the model is not a bound in either direction
(optimistic on contention, pessimistic on barriers/stores, RAW-only).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ycles (schema v3) `StageProfiler` rounded every opcode up to a whole clock period before adding it to a bucket, so `n` sub-cycle opcodes billed `n` cycles instead of `ceil(n * duration / period)`. Because `Sum(div_ceil(t_i)) >= div_ceil(Sum(t_i))`, per-stage cycles and `total_profiled_cycles` were systematically inflated, worst for the DMA-heavy stages that are usually the interesting ones. `timing_overlay` was hardened against exactly this rounding bias; the profiler was not. It has been invisible so far only by coincidence: `hbm2_preset` gives a DRAM tCK of 1 ns, identical to `PERIOD`, so every event lands on a cycle boundary and the two forms agree. Switching to `ddr4_preset` (tCK ~833 ps) or changing the core clock would have made `cycle_accounting_status` report a mismatch for pure rounding reasons, with no way to tell that from a real accounting hole. `StageRuntime` and `ResourceRuntime` now accumulate picoseconds and round once per reported quantity. Each level emits both: `*_picos` (exact, additive) and `*_cycles` (the rounded display view). They cannot both be additive -- buckets round independently, so bucket cycles can exceed the parent's by up to one cycle each -- so `PROFILE_CAVEAT` and a new `time_unit_status` say plainly: do arithmetic on picos, display cycles. `cycle_fraction` is now computed from picos so the per-stage fractions still sum to 1. `cycle_accounting_status` compares picoseconds, making the verdict exact and independent of the clock period. Added `resource_accounting_status`, which asserts the resource buckets partition the profiled time -- a real check that `resource_kind_for_opcode` neither drops nor double-counts an opcode class. Dropped `StageProfiler::duration_to_cycles` (its only remaining caller was its own test) in favour of the free `picos_to_cycles`. Verified on gpt_oss_moe_expert: schema_version 3, profiled picos == simulation picos (4516000), resource buckets sum exactly, cycle_fraction sums to 1.0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…annot see
The existing guard measures `unclassified_fraction`, which only catches a
compiler comment rename that matches *nothing* and drops opcodes into `Other`.
The other failure mode is invisible to it: if a renamed comment happens to match
a different rule in `classify_comment`'s priority chain, the opcodes stay
classified -- just in the wrong stage -- and the unclassified fraction never
moves. Given how much the chain leans on conjunctions, one negation and stateful
carry-over ("vram matrix mul" appears in two branches, resolved only by order),
that is a realistic outcome of an innocuous rename upstream.
Declared the classifier's comment substrings as `STAGE_VOCABULARY` data and
report which of them the ASM actually contained. A rename now shows up as a term
disappearing from `vocabulary_terms_present`, whatever it re-matched. The list is
for reporting only -- `classify_comment` keeps the rules, since a flat table
cannot express them.
`gpt_oss_moe_expert_test` (in CI) now asserts both guards. The vocabulary check is
a subset test on the 8 terms this single-expert program exercises: emitting an
extra recognised comment is harmless, dropping one is the drift being hunted. The
routing/gather/scatter terms are legitimately absent from this program.
Added `classify_comment_pins_the_stage_vocabulary`, which locks in behaviour on
representative real comment lines including the order-sensitive and stateful
cases, so inserting or reordering a rule cannot silently re-home opcodes that
already matched an earlier one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
qichao-arlo-wang
marked this pull request as ready for review
July 26, 2026 18:39
…efects
Three defects predating the timing-model work, surfaced by review. All are
separated here from the corrections to that work so they can be cherry-picked or
reverted on their own.
`M_MM_WO` and `M_MV_WO` were classified with an empty read set on the stated
grounds that write-out ops "carry no matrix/vector input operands". They are
read-modify-writes: `mm_wo` reads `vec_base + i * mlen * stride_len` for each of
`blen` rows and `mv_wo` reads its destination row, splicing the accumulator into
a `blen`-wide slot before writing the row back. A prefetch filling those rows is
a genuine RAW dependency that was never retired -- worse, the write-out's own
duration was then credited as hiding capacity for the very prefetch it depended
on. `gpt_oss_moe_expert`, the program in CI, has three `M_MM_WO` and one
`H_PREFETCH_V`, so this is live rather than latent. `bmm_wo` / `bmv_wo` do
overwrite whole rows and keep the empty set.
`V_TOPK` reported a single VLEN row. `topk_softmax` walks the logits in
VLEN-sized chunks, so the 128-expert policy touches two; the span is now derived
from the rmask policy.
`classify_comment`'s route-weight branch claimed any comment containing
"vram matrix mul". That string is emitted by the compiler's general-purpose
`VRAM Matrix Mul` helper, not the routed-MoE emitter, and the disjunct was
unconditional -- which also made the guarded copy further down (the one that
keeps activation-region multiplies in `ExpertActivation`) unreachable. On
gpt_oss_moe_expert it flipped the stage mid-activation and then carried over,
billing 53 instructions to `expert_route_weight` in a program with no routing at
all. The genuine route-weight comment is "materialize route weight", already
caught by the first disjunct.
Guarding the last one needed a signal neither existing check provides: a rename
that re-homes opcodes into a different *existing* stage leaves every vocabulary
term present and the unclassified fraction unmoved. The profile now emits
`classification.stage_instruction_counts`, and the CI test asserts the stages
this program cannot contain are empty and the ones it must have are not.
Reintroducing the rule fails it with exactly `{'expert_route_weight': 53}`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Corrections to this PR's own three commits. The pre-existing defects review also
surfaced are in the preceding commit.
UNSOUND PREMISE
The zero-elapsed guard rested on "every real read awaits mram/vram, which
advances the clock". False: `Cell::resolve_with` returns without awaiting when the
cell is `Ready`, so SRAM reads cost nothing on their own and all opcode duration
comes from explicit `cycle!` / `MatrixCore::compute` calls. A latency knob
configured to zero would have silently dropped real dependencies. Replaced with
explicit `V_RED_SUM { rd: 0 }` / `V_RED_MAX { rd: 0 }` arms mirroring `do_ops`,
which is what the guard was standing in for.
TAUTOLOGIES REMOVED
`resource_accounting_status` could not fail: `record` adds the same `wall_picos`
to one bucket and to the profiled total, so the equality holds by construction and
is insensitive to the misattribution it claimed to detect. It is now a
`debug_assert` that earns its keep only if someone adds a bucket that is a
sub-view of another. `total_stage_wall_picos` / `_cycles` were likewise always
equal to `total_profiled_*`. `cycle_fraction` and `time_fraction` became the same
ratio once both derived from picoseconds; only `time_fraction` survives, computed
exactly. `total_unprofiled_cycles` is now the additive complement so the
profiled/unprofiled/simulation triple adds up, which is the one place a reader
expects cycles to.
DIAGNOSTIC HOLES
Retired and barrier-discarded prefetch time appeared in no reported field --
neither `hidden` nor `pending` -- while the module doc claimed the model surfaces
un-hideable prefetch time. Added `retired_prefetch_picos` and
`discarded_prefetch_picos`; a test asserts every issued picosecond lands in
exactly one of the four. `dependent_prefetch_stalls` split into
`compute_prefetch_stalls` / `store_prefetch_stalls`, because `H_STORE_V` was a
`Barrier` on main and retired silently, so one scalar would have jumped across the
change for taxonomy reasons alone. The summary emits picoseconds beside cycles,
since `hidden_prefetch_cycles` is a residual and rounds sub-cycle hiding to zero.
COVERAGE
`timing_access_for_opcode` had no tests -- reverting `H_STORE_V` to `Barrier`, or
swapping `rd` for `rs1`, left the suite green. Extracted as the free function
`classify_timing_access` over a register accessor, with tests for the store range,
prefetch extents, write-out reads, topk policy span, fp0 no-ops and `M_BMV`'s
`rs1 + rd` addressing. Nothing drove `StageProfiler::record` -> `to_json` either,
so the picosecond fix could have been reverted in the caller undetected; added
sub-period accumulation and unprofiled-time tests, plus a CI assertion on
`cycle_accounting_status`, which is what actually catches a caller passing cycles.
The classifier vocabulary is now checked against `classify_comment`'s literals by
parsing the source rather than by author diligence; six previously shadowed terms
gained pinning cases; `extract_pair_id`'s `pair=` / `step6_pair` are declared and
reported; and vocabulary presence is scanned over comment lines only, since that
is all the classifier ever sees. Dropped `_sigmoid` from the expected vocabulary
-- it is in the ASM only because this test names a tensor `gate_sigmoid`, and its
rule is a conjunction that never fires here, so asserting on it would blame the
compiler for a local rename.
Also removed the exhaustive match's `_` wildcard claim that scalar and control
opcodes "touch no matrix/vector SRAM": `S_MAP_V_FP` writes a VLEN row. Classifying
it as `Other` is still right (the model tracks RAW only), but the reason given was
wrong.
DOCS
Corrected: "fully hideable" (hiding is capped at the compute's own duration), the
`Compute` variant description (four members have empty read sets), the `Store`
rationale (declining to credit its duration is a conservative choice, not a
bandwidth argument -- the model already assumes prefetches survive across stores),
`ResourceKind`'s stale "wall-cycle" wording, the `M_MM_WO` operand list, and
`PROFILE_CAVEAT`, which asserted `resource_proxy_*` buckets sum while two
sentences earlier saying cycles are not additive, and misstated the non-additivity
bound. Added `period_picos` so a consumer holding only the JSON can convert
between the two views.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
qichao-arlo-wang
force-pushed
the
feat/timing-model-hardening
branch
from
July 26, 2026 18:45
a3f3d42 to
ad76d58
Compare
qichao-arlo-wang
added a commit
that referenced
this pull request
Jul 27, 2026
Rebasing onto main drops this branch's copy of the emulator commit (a superseded duplicate of 2539533) and picks up schema v3 from #99, which changed what the profile emits. Three of the fields this harness read no longer exist, and one of its sums is now explicitly forbidden. `total_stage_wall_cycles` was removed as tautological -- `record` adds each opcode's time to exactly one stage bucket *and* to the profiled total, so it was always identical to `total_profiled_cycles`. `summarize_run` now reports the profiled figure, in both picoseconds and cycles. `cycle_fraction` became `time_fraction`: once both derived from picoseconds they were the same ratio, so only the exact one survives. `ramulator_proxy` was removed back in #98 -- it was incremented with the identical value as `dma`, so it carried no information. Both were read through `.get()`, so they had been silently producing `None` columns rather than raising. `_sum_stages` added per-stage `wall_cycles` across five routing stages. Under v3 each stage rounds up to a whole period independently, so `n` stages over-report by up to `n-1` cycles; this is exactly the cross-level cycle arithmetic PROFILE_CAVEAT now forbids. It sums `wall_picos` and rounds once, falling back to the old field for pre-v3 profiles rather than silently reporting zero. `timing_validation_gates`'s stage-accounting check chained an equality through the removed field, which would have made it permanently false. It now asserts `cycle_accounting_status == "profiled_time_matches_total"`, which v3 computes in picoseconds and is therefore exact and independent of the clock period. Also passed `dump_cwd` to `run_emulator_repeat_gate` in the Qwen3 replay. #99 added that parameter precisely for this caller; without it the repeat runs fall back to the shared emulator directory, so concurrent campaign workers race on vram_dump.bin / fpsram_dump.bin and copy each other's dumps into their own build directories. The main run already isolated itself. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
qichao-arlo-wang
added a commit
that referenced
this pull request
Jul 31, 2026
[Group B — MoE timing-replay harnesses · 4 of 4] The emulator work these harnesses were written against landed on `main` independently as #98, and was hardened further in #99, which changed what the profile emits. Three of the fields this harness read no longer exist, and one of its sums is now explicitly forbidden. `total_stage_wall_cycles` was removed as tautological -- `record` adds each opcode's time to exactly one stage bucket *and* to the profiled total, so it was always identical to `total_profiled_cycles`. `summarize_run` now reports the profiled figure, in both picoseconds and cycles. `cycle_fraction` became `time_fraction`: once both derived from picoseconds they were the same ratio, so only the exact one survives. `ramulator_proxy` was removed back in #98 -- it was incremented with the identical value as `dma`, so it carried no information. Both were read through `.get()`, so they had been silently producing `None` columns rather than raising. `_sum_stages` added per-stage `wall_cycles` across five routing stages. Under v3 each stage rounds up to a whole period independently, so `n` stages over-report by up to `n-1` cycles; this is exactly the cross-level cycle arithmetic PROFILE_CAVEAT now forbids. It sums `wall_picos` and rounds once, falling back to the old field for pre-v3 profiles rather than silently reporting zero. `timing_validation_gates`'s stage-accounting check chained an equality through the removed field, which would have made it permanently false. It now asserts `cycle_accounting_status == "profiled_time_matches_total"`, which v3 computes in picoseconds and is therefore exact and independent of the clock period. Also passed `dump_cwd` to `run_emulator_repeat_gate` in the Qwen3 replay. #99 added that parameter precisely for this caller; without it the repeat runs fall back to the shared emulator directory, so concurrent campaign workers race on vram_dump.bin / fpsram_dump.bin and copy each other's dumps into their own build directories. The main run already isolated itself. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
qichao-arlo-wang
added a commit
that referenced
this pull request
Aug 3, 2026
[Group B — MoE timing-replay harnesses · 4 of 4] The emulator work these harnesses were written against landed on `main` independently as #98, and was hardened further in #99, which changed what the profile emits. Three of the fields this harness read no longer exist, and one of its sums is now explicitly forbidden. `total_stage_wall_cycles` was removed as tautological -- `record` adds each opcode's time to exactly one stage bucket *and* to the profiled total, so it was always identical to `total_profiled_cycles`. `summarize_run` now reports the profiled figure, in both picoseconds and cycles. `cycle_fraction` became `time_fraction`: once both derived from picoseconds they were the same ratio, so only the exact one survives. `ramulator_proxy` was removed back in #98 -- it was incremented with the identical value as `dma`, so it carried no information. Both were read through `.get()`, so they had been silently producing `None` columns rather than raising. `_sum_stages` added per-stage `wall_cycles` across five routing stages. Under v3 each stage rounds up to a whole period independently, so `n` stages over-report by up to `n-1` cycles; this is exactly the cross-level cycle arithmetic PROFILE_CAVEAT now forbids. It sums `wall_picos` and rounds once, falling back to the old field for pre-v3 profiles rather than silently reporting zero. `timing_validation_gates`'s stage-accounting check chained an equality through the removed field, which would have made it permanently false. It now asserts `cycle_accounting_status == "profiled_time_matches_total"`, which v3 computes in picoseconds and is therefore exact and independent of the clock period. Also passed `dump_cwd` to `run_emulator_repeat_gate` in the Qwen3 replay. #99 added that parameter precisely for this caller; without it the repeat runs fall back to the shared emulator directory, so concurrent campaign workers race on vram_dump.bin / fpsram_dump.bin and copy each other's dumps into their own build directories. The main run already isolated itself. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #98, closing the four known limitations its description listed. One
commit per problem.
1. The overlap model had already drifted from execution (
09c250b)timing_access_for_opcodehand-mirrors the SRAM addressing of the execution armsin
do_ops, with no shared source of truth. Auditing the two side by side turnedup a divergence:
do_opshas zero-time no-op arms —V_RED_SUM { rd: 0 }andV_RED_MAX { rd: 0 }discard their fp0 write-back and return without touchingvram. The overlay classified those opcodes by shape alone and reported a read
range anyway, and
recordranretire_dependent_prefetchesunconditionally. Ano-op could therefore retire a prefetch it never read, permanently forfeiting the
chance to hide it and inflating
dependent_prefetch_stallsandcompute_ops.The error was conservative rather than flattering. No checked-in ASM emits
V_RED_SUM f0, so this was latent rather than observed — but it is exactly thedrift this mirroring invites, and review found two more instances of it that are
not latent (see §5).
The first attempt keyed on observed elapsed time — "an opcode that consumed no
simulator time performed no SRAM access". Review showed that premise is false
(
Cell::resolve_withreturns without awaiting on aReadycell, so SRAM readscost nothing and all duration comes from explicit
cycle!calls), so a zerolatency config would have silently dropped real dependencies. What shipped is the
explicit mirror:
V_RED_SUM { rd: 0 }/V_RED_MAX { rd: 0 }arms matchingdo_ops. See §5.Two structural guards on top:
_ => Otherwildcard is gone, so a newlyadded opcode fails to compile until someone classifies it rather than being
silently absorbed into "touches no SRAM".
guard catches: a wrong address or extent.
2.
H_STORE_Vwas modelled as a barrier (09c250b)It mapped to
Barrier, which abandoned every pending prefetch. It is reallyan SRAM read (
src_addr = gp(rd), extentVLEN * STORE_V_AMOUNT) feeding an HBMwrite, so a program interleaving stores would get essentially no overlap credit.
(No ASM currently checked into
testbench/containsH_STORE_V, so this too is acorrectness fix ahead of the workloads that need it, not a measured regression.)
Added a
Storeaccess kind that retires the prefetches it genuinely depends onbut does not hide pending prefetch time. That last part is a conservative choice,
not a derived one: the model already lets a prefetch issued before a store survive
across it and be hidden by a later compute, which assumes the prefetch is in
flight while the store runs — so "they contend for DMA" would equally forbid
that. Without a bandwidth model there is no principled answer, and declining to
credit the store's own duration is the direction that cannot overstate achievable
overlap.
C_BREAKstays a real barrier.The module docs now state plainly that the result is not a bound in either
direction: optimistic on contention (no DMA bandwidth, queue depth, SRAM port
or HBM channel modelling, unbounded pending queue), pessimistic on barriers and
stores, and RAW-only so writes by compute are invisible. It is useful for
relative comparison between programs on the same model, not as a hardware figure.
3. Stage profile time rounded per opcode (
8afaf40)StageProfilerrounded every opcode up to a whole clock period before addingit to a bucket, so
nsub-cycle opcodes billedncycles instead ofceil(n * duration / period). SinceΣ div_ceil(tᵢ) ≥ div_ceil(Σ tᵢ), per-stagecycles and
total_profiled_cycleswould be systematically inflated.timing_overlaywas hardened against precisely this bias in #98; the profiler wasnot.
StageRuntimeandResourceRuntimenow accumulate picoseconds and round once perreported quantity. Every level emits both
*_picos(exact, additive) and*_cycles(the rounded display view). They cannot both be additive — bucketsround independently, so bucket cycles can exceed the parent's by up to one cycle
each, and a set of
nsiblings by up ton-1— soPROFILE_CAVEATand a newtime_unit_statussay it outright: do arithmetic on picos, display cycles.time_fractionis computed from picos so per-stage fractions sum to exactly 1;the old
cycle_fractionbecame the same number and was dropped. A newperiod_picosfield lets a consumer holding only the JSON convert between the twoviews.
cycle_accounting_statusnow compares picoseconds, making the verdict exact andindependent of the clock period.
Note the inflation this fixes has never actually been observed.
hbm2_presetgives a DRAM tCK of 1 ns, identical to
PERIOD(both measured:period=1.000nsfor HBM2_2Gbps,
833psfor DDR4_2400), so every event lands on a cycle boundaryand
div_ceilis exact today. Switching preset or core clock breaks that, andcycle_accounting_statuswould then have reported a mismatch for pure roundingreasons, indistinguishable from a real accounting hole. When it does break, the
error concentrates in stages with many short opcodes — the per-opcode rounding
error is at most one cycle regardless of opcode length — not in the long DMA ops.
Schema bumped to v3.
4. The classification guard could not see misclassification (
14ce854)#98's guard measures
unclassified_fraction, which only catches a compilercomment rename that matches nothing and drops opcodes into
Other. The otherfailure mode is invisible to it: a renamed comment that happens to match a
different rule in
classify_comment's priority chain keeps the opcodes classified— just in the wrong stage — and the unclassified fraction never moves. Given how
much that chain leans on conjunctions, one negation, and stateful carry-over
(
"vram matrix mul"appears in two branches, resolved only by order), that is arealistic outcome of an innocuous upstream rename.
Declared the classifier's comment substrings as
STAGE_VOCABULARYdata and reportwhich of them the ASM actually contained. A rename now surfaces as a term
disappearing from
vocabulary_terms_present, whatever it re-matched. The list isfor reporting only —
classify_commentkeeps the rules, since a flat table cannotexpress them.
gpt_oss_moe_expert_test(already in CI) asserts these guards. The vocabularycheck is a subset test — emitting an extra recognised comment is harmless,
dropping one is the drift being hunted. The routing/gather/scatter terms are
legitimately absent from this single-expert program. Review then showed this pair
is still not enough; see §5.
Also added
classify_comment_pins_the_stage_vocabulary, locking in behaviour onrepresentative real comment lines including the order-sensitive and stateful
cases, so inserting or reordering a rule cannot silently re-home opcodes that
already matched an earlier one.
5. Review round (
afb66bd,ad76d58)Five reviewers went over the three commits above. Their confirmed findings are
fixed in two follow-up commits, split by provenance so the pre-existing defects
can be cherry-picked or reverted independently of the corrections to this PR's
own work.
afb66bd— defects that predate this PRM_MM_WO/M_MV_WOreally do read VRAM. They are read-modify-writes, sothe empty read set (and the comment defending it) was wrong, and the write-out's
own duration was being used to hide a prefetch it depended on. This fires on
gpt_oss_moe_expert, the program in CI.V_TOPKreads two VLEN rows under the 128-expert policy, not one.classify_commenthad a dead rule mis-attributing ~25% of opcodes. Itsroute-weight branch claimed
"vram matrix mul", a string emitted by thecompiler's general-purpose matrix helper. That made the guarded copy below it
unreachable and billed 53 instructions to
expert_route_weightin a programwith no routing. Removed, and guarded by the new
stage_instruction_countssignal, since neither existing check can see a rename that re-homes opcodes into
a different existing stage.
ad76d58— corrections to this PR's own commitsReadycell without awaiting, so elapsed time tracks
cycle!calls, not memorytraffic; a zero latency config would have dropped real dependencies. Replaced
with explicit no-op arms.
resource_accounting_statuswas a tautology, as weretotal_stage_wall_*;cycle_fractionandtime_fractionhad collapsed intothe same number. Removed, with the invariant kept as a
debug_assert.retired_prefetch_picos/discarded_prefetch_picos, with a test that everyissued picosecond lands in exactly one bucket, and split the stall counter by
cause so it does not jump across the
Barrier→Storechange.timing_access_for_opcodenorStageProfiler::record→to_jsonhad any test. Both changes were revertible with the suite green. Both now
covered, plus a CI assertion on
cycle_accounting_status— the only thing thatcatches a caller passing cycles instead of picoseconds.
classify_comment's ownliterals rather than relying on author diligence;
extract_pair_id'spair=/step6_pairare declared and reported; presence is scanned overcomment lines only. Dropped
_sigmoidfrom the expected set — it is in the ASMonly because this test names a tensor
gate_sigmoid, and its rule is aconjunction that never fires here.
Negative tests
Every fix was verified by reintroducing the defect and confirming a specific
failure, since the reviewers' central criticism was that these changes were
silently revertible:
classify_commentdead rule{'expert_route_weight': 53}profiled 4516 ps of 4516000 psH_STORE_V→Barrierstore_reads_the_region_it_drains_rather_than_acting_as_a_barrierM_MM_WO→ empty read setvector_write_out_ops_read_their_destination_rowV_TOPK→ single rowtopk_reads_every_row_the_expert_policy_spansPR #97 rebase notes
#97 will be rebased onto this. Its emulator-side commit (
124986e) is asuperseded duplicate of
2539533and should be dropped; what survives is themoe_timing/testbench. Every field access in it was checked against the v3output — the exact list, verified line by line:
replay/utils.py:121total_simulation_cyclesreplay/utils.py:122total_stage_wall_cyclestotal_profiled_cycles) — returnsNone. Usetotal_profiled_picos.replay/utils.py:123cycle_accounting_statusprofiled_cycles_match_total→profiled_time_matches_total)replay/utils.py:134resource_proxy_cyclesresource_proxy_picosfor anything arithmeticreplay/utils.py:140wall_cycleswall_picosif summedreplay/utils.py:141cycle_fractionNone. Renamed totime_fraction(same value, now exact).replay/utils.py:152resources.get("ramulator_proxy")None. It was definitionally equal todma.qwen/export_pilot_results.py:36total += stage.get("wall_cycles")routing_tax_cyclesover-reports by up to 4. Sumwall_picos, round once.qwen/export_pilot_results.py:92,94,95campaign/run_subset.py:1262Counter(cycle_accounting_status)replay/timing_validation_gates.py:240-241== total_stage_wall_cyclestotal_profiled_picosagainsttotal_simulation_picosinstead — that is exactly whatcycle_accounting_statusreports.Also re-run
timing_validation_gates.py'sg2_prefetch_compute_overlapafter therebase: this PR changes the overlay (
Store, explicit no-op arms, split stallcounters), and that gate is a hard pass/fail on
overlay_hidden_cycles > 0. Themicrobenchmark has no stores and no zero-duration compute, so it should hold, but
it has not been re-run against these commits.
Still not fixed## Still not fixed
wrong address or extent. The real fix is an access descriptor owned by
op::Opcodeand consumed by both paths.so means rebuilding the determinism guarantees Deterministic emulator timing, stage profile v2, and experimental overlap model #98 established, under
concurrency.
guards detect drift; neither removes the coupling. That needs the compiler to
emit a structured stage map.
Verification
cargo build --release— clean, no warningscargo test --workspace --release— all suites pass (main binary 59 → 89 tests)cargo fmt --all -- --check— cleanruff format --check ./ruff check .— cleanjust test-routed-moe-expert— PASSED,12.2% unclassified, 8/27 vocabulary terms present, distribution {expert_weight_prefetch=9, expert_projection=831, expert_activation=222, other=25}classify_commentbug makes the newdistribution guard fail with
{'expert_route_weight': 53}stage_profile.jsonchecked by hand:schema_version: 3,total_profiled_picos == total_simulation_picos(4516000),profiled_time_matches_total,resource_buckets_sum_to_profiled_time, resourcepicos sum exactly,
cycle_fractionsums to 1.0🤖 Generated with Claude Code