Conversation
Step 1 of the correctness PR is a source audit at pinned revisions; no code runs and no accuracy is claimed. Findings that change the plan: - The round-robin DP lane defect is still present on main, and main already implements the intended formula for the DECODE role only, so the fix is an internal-consistency repair. - The shared monolithic forward is still split by local request phase at three layers, and the candidate's decode metrics hunk calls a method main has deleted, so that hunk is blocked pending a rewrite. - The opt-in DP placement strategy reuses a step identity that is not valid for dense models or for MoE with DP>1 before the shared forward fix, so the order becomes RR, then shared forward, then DP placement. - Two routing implementations can already select each other's cost model on main through a shared training signature that carries no routing term, reachable from the public CLI in a PDD run. - The legacy fused-MoE path omits gated activation and the local top-k reduction; adding the reduction changes what the measurement contains and no existing column separates old rows, so it needs a decision. Also records a defect in the candidate itself: it deletes a method the SGLang scheduler still calls.
fwyc0573
force-pushed
the
refactor/oversized-module-split
branch
from
September 21, 2026 14:06
41dabfb to
56efdf7
Compare
fwyc0573
force-pushed
the
fix/issue26-correctness-pr
branch
from
September 21, 2026 14:06
4ca4abe to
a1f8e45
Compare
The four modules this PR edits are now under the 2,000-line gate, and the split is verified behavior-preserving: 67 of 67 fidelity cases identical with no predictor cache name differences, measured from a detached checkout of the split branch's tip. Merging rather than rebasing keeps the published history additive, so the review anchors on this branch stay valid.
Related to #26. `_schedule_batch_mode` derived the replica index from a counter that persists across calls but the DP lane from the request's index within the current call. The lane therefore restarted at zero every time `schedule()` was entered, so an identical ordered request stream landed differently depending only on how it happened to be divided between calls. With one replica and two lanes, admitting requests one at a time put every request on lane 0. Both values now come from one persistent ordinal. This is the rotation `_schedule_decode_lane_round_robin` already applies to the unified decode role, so the change makes the monolithic and prefill paths consistent with a formula already in this file rather than introducing a policy. The per-replica grouping of the returned mapping is unchanged, and a test asserts that specifically; it passes on the pre-fix code too. Four regression tests cover call partitioning, non-contiguous replica ids, DP1 through DP4, an empty scheduling call and the return order. Their sensitivity was verified by stashing the fix and rerunning: three fail on the pre-fix code for the right reason, lanes collapsing to lane 0. test_replica_identity_contract pinned the literal expression that changed. What it guards is that a non-FFN cluster scheduler takes the lane modulo the Replica-local DP size rather than a global or expert-parallel cardinality, so it now asserts that property over every lane assignment instead of matching one expression. It still fails if the lane is ever taken modulo anything else. Unit comparison against the refactor tip over 73 files: identical failure identities, four new passes.
…verage The four DP placement cases that discriminate the round-robin rotation fix live in the fidelity case table on the refactor branch. Without this merge the correctness branch's own harness still has the 67-case table, so measuring it with its own tooling would exercise a table that cannot see the fix. Measuring a commit with a harness from a different branch works but is only reproducible by someone who repeats that pairing.
The audit recorded that forward_sync_state partitions the step-id namespace by synchronization kind. It does not. _next_step_id_by_replica is keyed by replica alone, so allocation is already Replica-scoped and monotonic across prefill and decode. What is partitioned is _open_steps_by_kind, the open-step binding table, and the waiting room beside it. The defect is unchanged: a mixed-phase forward still puts one required lane in each table and stalls. But the change Step 3 has to make is narrower than the audit implied, and the reason the candidate's report-order key is invalid for MoE with DP above one is different: the ids come from one counter, and the lanes never reach a shared step to key a report on. Verified against the source at c18eb2c rather than taken from the earlier reading.
…sm and R34-03 remedy
The parent branch carries three commits this branch needs before its own W2 measurement can mean anything: the gate correction (a comparison that compared nothing reported IDENTICAL), the retained split boundary checks, and the final 71-case acceptance record. This branch's earlier W2 fidelity runs were taken with the pre-correction harness, so they are re-measured on top of this merge rather than cited.
Review comment R35-01: the round-robin tests compared one run against another, which shows placement does not depend on how the stream is divided but would also hold for a wrong rule applied consistently, and the identity test reads source text, which cannot tell a correct expression from an equivalent one it does not recognise. Three topologies now have their full rotation written out by hand, past the wraparound, for the case where both the replica and the lane advance. The tests drive the public schedule() instead of _schedule_batch_mode, so the cluster-type dispatch is exercised rather than assumed, and each runs for both roles that reach batch-mode placement, MONOLITHIC and PREFILL. Requests now carry increasing arrival times, so the sort_requests() that schedule() performs first orders the queue itself rather than leaning on a stable sort over equal keys. Re-running these against the pre-fix method, taken verbatim from 6ab521d^ and installed in memory, fails 12 of 22 and leaves the grouping control and the eight unrelated lane tests passing. The failures are informative: the old code produces the expected sequence exactly when the whole stream arrives in one call, in all three topologies, and collapses to lane 0 only when requests arrive one at a time. The hand-derived expectation therefore agrees with the rotation that already existed; the fix made incremental placement match it rather than introducing a policy. The source-text guard stays, with a docstring saying it is governance only and must not be satisfied by rewording a correct expression. The fidelity case docstring claimed prefill placement "therefore has to be validated by unit tests". That understated the remedy: the wrapper limit is not a runtime limit, and full coverage needs a fixture that builds a runtime configuration directly with durations injected at the predictor boundary. It now says so, and points at the scheduler-level tests for what is covered today.
The first W2 measurement ran the 6ab521d source against the refactor tip's case table and comparator, with the harness revision disclosed in prose rather than recorded as a field, and it was taken with the harness that could report success without comparing anything. Neither fact impugned the result; both made it a poor record. Repeating it costs less than arguing about it. Baseline 6ef0a3c against candidate ceac2b4, both clean detached checkouts, one harness at ceac2b4, no filter, clean cache, 71 executed and 426 cache files on each side. 71 of 71 compared, 68 identical, and the mismatch set is exactly the three cases predicted to move before measuring. No provenance findings, no cache differences. Lane occupancy reproduces the first measurement number for number: collapsed onto lane zero before, spread after, both offline controls untouched. The re-measurement changed the provenance of the evidence, not the evidence. Exit code 1 is the correct outcome here. The gate reports inequality; the acceptance criterion for a behavior fix is the stated expectation. Also adds the guard that keeps the new tests' coverage claim honest. TRANS falls through the same dispatch as MONOLITHIC and PREFILL, so the role list is only complete because TRANS is declared and never constructed anywhere in frontier/; the guard fails if that changes or if a new role appears. review.md gains a remediation table, one row per accepted comment with the artifact that closes it, and the W2 negative control's more interesting result: the pre-fix code produces the expected sequence exactly when the whole stream arrives in one call.
…hases
A monolithic Replica runs prefill and decode on the same attention-DP lanes,
so one forward step can hold a prefill batch on one lane and a pure-decode
batch on another. The two phases were given disjoint waiting rooms and
disjoint open-step namespaces, so those lanes waited in different rooms,
neither reached the expected lane count, and the idle-lane filler could not
rescue either one because each sibling stage was busy holding the other
phase. The forward never dispatched. Reproduced end to end: with
attn_tp=1, attn_dp=2, moe_tp=1, moe_ep=2 the sequential run ends with a
non-empty scheduler state.
The fix is one lifecycle, applied as a unit rather than as a shared room
with phase-specific completion:
- MONOLITHIC allocates one waiting room, bound to the forward name and to
both phase names, and resolves its lanes in one "forward" step-id space.
- The two near-duplicate per-layer sync entries become one
`enter_layer_sync(..., mode)`, mirroring the existing
`schedule_layer_wave(mode=...)`.
- `forward_collective.py` completes a shared cohort once: it advances the
decode-phase requests once, restores the full-stage owners once, then runs
each live source through its own existing phase helper with `direct_batch`,
so every lane predicts its continuation from its own batch.
- The post_moe collective event class is chosen from cohort contents, not
from whichever lane closed the room. `EventType` values are priorities, so
a pure-prefill and a pure-decode cohort keep exactly the event type they
have today; only the mixed cohort, which could not complete at all, is new.
- A request may no longer occupy two non-idle source lanes of one forward.
- A dense layer inside a MoE model labels each source by its own phase and
keeps the prefill component ledger to prefill sources.
- The shared completion makes the legacy-aggregate check once per source. The
per-phase helpers only make it when they pop the room themselves, and
delegation hands them `direct_batch`, so the check would otherwise be lost
on exactly the path that now owns it.
Coverage: 24 behavior tests over every phase pairing, both arrival orders,
unequal source tokens, idle participation, duplicate ownership, successive
layers, a decoding request inside a prefill batch, dense-layer transitions
and a disabled metrics store; plus a direct-construction integration test
that runs the real Simulator event loop over a multi-lane monolithic MoE
configuration and reaches four mixed-phase cohorts.
Controls, each failing for its own reason: the pre-fix tree deadlocks
("Sequential simulation ended with non-empty scheduler state"); borrowed
source timing trips "one attention-DP lane cannot occupy two open sync
cohorts"; a repeated layer advance trips "Decode post_moe layer counter
cannot advance"; a repeated ownership restoration trips "operation_id is
already queued or active".
tests/unit and tests/integration show the same failures as HEAD -- 84 and 5
pre-existing, identical identities, no regressions -- with 23 and 1 new
passes.
The 71-case matrix ran from two clean detached worktrees, both driven by one harness revision: baseline 3d47417 against candidate 65ed8a7, source_dirty False on both sides, no case filter, clean cache, 71 executed and 426 cache files each. 71 of 71 compared and 71 identical, with no provenance findings and no predictor cache differences. That is exactly what design.md predicted before the run, so its conditional I7 branch was not taken. Stated plainly in the records: no matrix case can reach a multi-lane monolithic MoE forward at all, so a null result is the pass condition for "nothing else moved" and not evidence that the defect is fixed. The evidence for that is the direct-construction runtime test. The four deliberate-defect control trees were rebuilt against the final test file, because they predated the EP wave marker guard and their recorded counts no longer matched what is delivered. Each tree now carries the final source and the final tests and differs by exactly one edit, except the baseline tree which carries the pre-fix parent in full. Counts are 22, 12, 2 and 16 of 23 unit tests, and each runtime failure remains distinct. One earlier claim is corrected where it was too strong: on the pre-fix tree the same-phase pairs do not pass outright. They pass every assertion about the forward itself and fail only at the final shared-room inspection, which the pre-fix cluster has no structure for. The mixed pairs fail earlier, at an empty collective list. The gap between the two groups is the deadlock.
Frontier routed requests to a Replica's attention-DP lanes by round-robin, which no vLLM deployment does: a vLLM V1 frontend picks an engine from a `waiting * 4 + running` score over counts it only sees when the coordinator publishes a snapshot. Placement therefore lags the true load, and the lag itself changes which lane a request lands on. `VllmDPLoadBalancer` models the two halves of that mechanism from vLLM v0.10.2: the frontend's selection (`core_client.py:1139-1153`, including the lowest-index tie break and the local waiting reservation that spreads requests between snapshots) and the coordinator's publication schedule (`coordinator.py:195-211`, the 100 ms changed / 5000 ms idle intervals, the 50 ms first-snapshot collection wait, and the latch that publishes the previous step's counts). Out-of-order step reports warn and still apply, as the reference does; nothing here aborts a run. The object creates no events, so a drained simulation still drains. `VllmLoadBalancingClusterScheduler` wires it to one co-location Replica. It is selected by `--cluster_scheduler_config_type vllm_load_balancing` and nothing else changes when it is not selected: `round_robin` remains the default. Supported scope is enforced in the constructor rather than documented -- one Replica, one pipeline stage, the `vllm_v1` replica scheduler, and a report key whose ordering actually holds. That last guard is measured, not assumed: a MoE Replica resolves one shared forward identity across its lanes, so the key is monotonic per Replica, while a dense Replica keeps a per-lane counter whose keys interleave under staggered arrivals. Dense therefore stays at attn_dp=1. Two seams carry it. `BaseClusterScheduler.schedule_at(time)` defaults to `schedule()`, so a placement policy that depends on elapsed time receives it as an argument instead of through a mutable bridge, and `on_replica_batch_end` is an inert hook called after the lane's request-state transition, so the reported load is the post-step one. `get_request_load()` reuses the existing decision-log waiting accessor, so a load balancer and the decision log cannot disagree about what is waiting. No placement or timing equivalence with a real vLLM deployment is claimed.
Adds the W4 test report, a Step 4 section in `validation.md`, the chronological progress entries, and the delivered-row and open-item-2 resolution in `review.md`. Also makes the W4 unit file order-independent. Running it after a MoE configuration in the same process tripped the process-global `IS_MOE` latch, because the file builds both dense and MoE shapes; it now resets the simulation globals around each test, as `tests/unit/test_config_owned_contracts.py` already does. The fidelity matrix was measured at `10dd474`, which predates this fixture, and is unaffected: no matrix case runs a unit test file.
The W5 collision needs two clusters in one run to resolve different MoE routing implementations. They cannot: `moe_routing_distribution_type` is declared once on `ReplicaConfig`, `ClusterConfig` declares no per-role override for it, so `get_field_value` always falls back to the global field, and the generated CLI exposes exactly one flag for it. Both recorded mechanisms are therefore latent code, not reachable behavior. Making them reachable requires adding the per-role override first, which is the configuration surface this step would then have to defend with a registry routing axis. Keeping the current contract instead: one global distribution, one derived routing path per run. Records what the module actually decides (which `moe.csv` rows train `moe_gating_routing_topk`) and the measured cost of picking the wrong path on the three datasets that carry both: 3.1% median, 7.3-7.8% and 4.2-5.4% of the summed per-layer operator medians. That is why the existing distribution-to-path mapping stays; it is not a gain from the reverted override, which changed nothing while unset. No source change. The drafted implementation is archived as a patch.
`_run_fused_moe_iteration` fed the second expert GEMM the gate half of the first projection, unactivated, and never reduced the per-expert outputs. vLLM's own `fused_experts_impl` runs GEMM1, a gated activation, the optional activation quantization, GEMM2 with the routing weights, and a local top-k reduction. Two operations were missing: `silu_and_mul` and `moe_sum`. The path is live. It is selected whenever vLLM exposes the low-level API, which the pinned reference v0.10.2 does and `environment_profiling.yml` pins with `vllm>=0.10,<0.11`. Estimated from the checked-in datasets, the omitted kernels are 16.5% of the corrected `moe_grouped_gemm` time at 4096 tokens on a800/qwen3-a3b-30b-moe, 6.8% median over its rows. Both new buffers are allocated once at the profiling site, outside the timed step, and the activation writes into a preallocated tensor rather than through the `SiluAndMul` wrapper, whose `forward` allocates on every call and would land inside the measured region. `moe_grouped_gemm` now means the complete local expert computation. That is already what the functional backend measured, since `fused_experts` returns reduced hidden states, so this removes a scope disagreement between the two backends rather than creating one. The reduction is a sum over one token's own experts; no communication cost is added or removed. No gated/non-gated branch: the profiler only ever materializes `w1` with `2 * E` rows, so a conditional would be unreachable. 7 CPU tests cover the arithmetic against a written-out reference, the comparison that distinguishes it from the old slice, call order and operand provenance, routing-weight placement, the FP8 quantizer input, workspace reuse, and allocation-site dimensions. Native GPU parity is NOT_RUN: no local environment selects this path.
The CPU boundary tests validate the composition of the repaired expert iteration with stubbed kernels. They cannot show that the real kernels agree with vLLM's own path, which is what the repair claims. Add a GPU parity test that drives `_run_fused_moe_iteration` with the same buffer shapes, kernel config and alignment that `profile_fused_moe_kernel` uses, and compares its output tensor against `fused_experts` at zero tolerance. It covers the production Qwen3-A3B-30B shape at 4096 and 4097 tokens on two expert-parallel shards, a smaller shape with a different top-k and experts that receive no tokens, repeated invocation, and the FP8 path as a structural check. Production shapes are read from the checked-in model config so they stay tied to the model Frontier profiles. The test runs only under CUDA with a vLLM build that exposes the low-level fused-MoE API, because that is the only configuration selecting the repaired path; every other environment skips. Also drop `expert_hidden_dim_per_partition` from the iteration signature. The gated activation reads the whole first projection, so the parameter no longer selects anything.
`moe_grouped_gemm` now means the complete local expert computation on both vLLM backends, but nothing in a profiled row says which version produced it. `moe_grouped_gemm_backend` returns `vllm_fused` for the low-level and the functional path alike, and `profiling_patch_tag` appears only in one historical CSV header with nothing in the source writing it. Per the maintainer's decision, leave the metadata alone and state the limit in the profiling guide instead: what the operator measures, how large the pre-repair gap was, and that a row cannot be checked for completeness from its own columns, so the remedy is to re-profile. Record the decision and its evidence in the task review and requirements.
…m backend An expert-parallel lane that routes no token in a step asks for an empty transfer: moe_operator_times computes data_size_bytes = embedding_dim * 2 * routed_tokens and hands it to predict_all_to_all, and predict_reduce_scatter floor-divides by the device count and reaches zero the same way. _validate_data_size accepts zero, so the request reaches the collective-sim runner, which rejected it as a missing field and aborted the run. The repair is entirely in the submodule. htsim_runner.py read zero as "field not provided" in two places, and neither it nor the scenario schema rejected a negative payload. This moves the gitlink from b8518af to eb7bc4f (fwyc0573/frontier-htsim, branch fix/zero-payload-input-handling, companion draft PR #1). No Frontier source changes. tests/unit/test_collective_sim_zero_payload.py covers the Frontier call path on the canonical TP=4 x DP=2, EP=8 pod: an empty all-to-all and an empty reduce-scatter each keep the 7 x 0.5 us NVLink synchronization latency, a populated all-to-all still costs more, and a negative payload is still rejected by Frontier's own guard. Against gitlink b8518af, 3 of the 4 fail with "Error: missing required fields: ['tensor_bytes']". The module skips when the optional submodule is not initialized and built, which is the default.
Three governance tests walk (repo_root / "frontier").rglob("*.py"). Once the
optional collective-sim submodule is initialized, that walk reaches 38 vendored
files, one of which does not parse under Python 3:
tests/unit/test_model_architecture_registry.py::test_raw_model_profile_resolution_callsites_are_allowlisted
IndentationError: expected an indented block after 'with' statement on line 24
frontier/cc_backend/backends/collective-sim/sim/EXAMPLES/in_and_out/process_data.py
The other two tolerated it — one catches SyntaxError, the other only searches
text — but both silently measured vendored files as if Frontier owned them, so
the scan result depended on whether a developer had run git submodule update.
tests/frontier_sources.iter_frontier_sources() yields the 413 files Frontier
owns and skips the vendored subtree; all three scans go through it. With the
submodule initialized and built, tests/unit returns to its 84-failure baseline
with 3782 passing, the four extra being tests/unit/test_collective_sim_zero_payload.py.
W6 closes: exp-0922-145047-660565 on an H800 under codesign returned 8 of 8 at rtol=0, atol=0 against vLLM 0.10.2's own fused_experts. The report also records the three earlier attempts and their causes, since two of them are image facts the next native run needs: vllm/vllm-openai:v0.10.2 ships neither pytest nor nvidia-smi and keeps its injected driver at /usr/local/nvidia/lib64 off the loader path, the httpproxy recipe returns 407 for pip so worker packages come from the internal mirror, and logs_rjob returns empty for these jobs while get_rjob_infos plus logs_replica returns the container lines. W7 closes: the user authorized the companion-repository option, the backend fix is published as fwyc0573/frontier-htsim eb7bc4f with draft PR 1, Frontier moved its gitlink, and both negative controls hold. A new report covers the defect, the repair, both controls, the clean-checkout validation, and the governance scan the bump exposed.
…licy The implementation list named three of the six registered cluster schedulers. This adds the two sticky variants that were already registered and the new vllm_load_balancing policy, with the scope its constructor actually enforces: one co-location replica, vllm_v1, one pipeline stage, and either a MoE model or attn_dp=1. It also states what round-robin distributes over, since this PR changed that rotation to persist across scheduling calls. The eight pre-existing documentation-contract failures are unchanged by this edit; their FAILED list is identical before and after.
… findings Step 8 §14.1 ran the selected suites together on the integrated branch instead of package by package, and exercised the first-load predictor-cache path deliberately rather than inheriting a warm cache. - Unit: 84 failed, 3782 passed, 49 skipped, 11 errors. The FAILED set is identical to the recorded origin/main baseline in both directions. - Integration: 15 passed, 22 skipped, 5 errors. The added skip is the W6 GPU parity module; the 5 errors are the absent pinned PD-AF Reference checkout and are identical on the base. - All 16 release-supported architecture examples pass across co-location, sequential PDD, and sequential PD-AF in offline and online modes. - Four PP=2 runs pass, putting the changed cluster-scheduling, stage-dispatch, and metrics code on the multi-stage path that the PP1-only DP placement policy cannot reach by itself. - The trained-predictor path run cold and then warm against an empty scratch cache produces byte-identical request metrics, so the persisted-cache path reproduces the freshly trained one exactly. The repository cache was neither moved nor deleted. Step 8 §14.2 records the final diff review, its method, and the cleanup done in that pass. future.md records two deferred items: re-pointing the collective-sim gitlink at main once the companion PR merges, and the pre-existing tests/debug/ pointer defect that AGENTS.md, a scheduler docstring, and 10 baseline unit failures share. That defect predates this branch and its repair is a decision about the published test surface, so it is reported rather than fixed here.
Records the §14.2 diff review method and the §14.3 PR hand-off, and writes summary.md: the stacked-PR layout, the per-work-package outcomes, the delivered source/test/record paths, the observed validation results, the two deferred items, and what this validation does not establish. The §14.2 review is stated as a self-review by the same agent that wrote the change, not an independent review.
…ment Record the user's request to remove the PP1 restriction of VllmLoadBalancingClusterScheduler inside this PR, the codesign-only GPU instruction, and the decisions D-a..D-g taken on 2026-09-22. plan.md gains amendment A12 and Step 9 (section 17): the pinned vLLM 0.10.2 count-publication semantics under the batch-queue stepping path, the equivalence argument that leaves the steady state unchanged, the schedule-only-iteration gap, the on_replica_batch_scheduled hook design, the report-key options, CPU packages P1-P6 and ground-truth packages G1-G5, the frontier-calibration v2 case binding, and the codebase-design framing. design.md gains the W9 analysis with the discriminating scenario derived from the score algebra. requirements.md keeps the verbatim requests and answers; progress.md records that no source or GPU action has started.
…xed batch A dense layer in a MoE model completes per source, outside the shared forward completion that credits a routed layer to every decoding request in the cohort. The decode handler credits its own batch, but the prefill handler credits nothing, so a request that had finished its prefill and was carried in a prefill-mode batch (chunked prefill mixes them) missed every dense layer: `MoE -> dense -> MoE` counted 1, 1, 2 instead of 1, 2, 3. `advance_decode_layer` now holds the validate-then-credit rule once, the shared forward and decode handlers use it, and `complete_dense_layer` credits the decoding members of a prefill-mode source before delegating. A request still prefilling has no decode layer to credit, and a PREFILL-role batch carries none, so the disaggregated roles are unchanged. Tests: a dense layer executed for a mixed source (+1 for the decoder, 0 for the prefiller, pure-prefill control credits nothing), the routed -> dense -> routed sequence, and a real-loop hybrid-layer variant whose credits are read before the token rollout resets them. On the pre-fix source that variant peaks four decode tokens at 3 of 4 layers. External review finding C35-01; evidence in task_memory/task_2026-09-21_issue26_correctness_pr/test_report_2026-09-22_review_corrections.md.
…ests without torch The FP8 native case passed `block_dims` but not `block_shape`, so the expert kernels read the block-quantized scales through their per-tensor path rather than the production block-quantized invocation that `profile_fused_moe_kernel` uses. Pass it, and pin on CPU that both GEMM invocations receive the block shape (and `None` when omitted), so a finite output cannot hide the wiring again. The CPU boundary module imported torch unconditionally and added a collection error to the minimal simulator environment; it now skips through `pytest.importorskip`. The W6 report restates the native result as seven reference comparisons at zero tolerance plus one FP8 structural check (FP8 numerics not established), replaces the legacy-equals-functional scope claim with a scope table (the functional entry aligns inside `fused_experts`, the low-level path aligns before the timed step), and the profiling guide says the two backends time different envelopes. The corrected native check is not re-run. External review findings C35-02, C35-03, C35-04.
…d the Step 9 plan Records: D2's scope-identifier clause marked superseded by the dated documentation-only decision; the W6 result stated as seven comparisons plus one FP8 structural check; the progress status table brought current; the review request and its scope recorded; a finding-by-finding disposition table (C34-01, C35-01..05, P9-01..06) with evidence; the verification report for packages B-E. Step 9 plan: section renumbered from 17 to 18 (the source index already held 17). The room-only hook rule and the "steady state needs no change" claim are replaced by the reference's engine-iteration state table and its preconditions; K1, K3 as written and stride keys are rejected as an acceptance basis, with the counterexample reproduced on this branch's balancer and six invariants for the rule to be chosen at a design checkpoint; the instrumentation covers the whole emission -> coordinator -> frontend -> routing chain; T1 uses a CPU reference-loop oracle and a causal join instead of boundary indices; the negative control is an explicit test-only variant because the unmodified constructor rejects PP2; PP3 gets a fixture with a valid layer count; the work graph and C1-C5 are revised. No Step 9 source change; execution not started.
…PR 35 description update
…inst the core-module quality gates The corrected FP8 native case (block_shape now forwarded) was re-run on one H800 under codesign at the user's authorization: 8 passed, exit 0. It remains a structural check; FP8 numerics are still not compared against a reference. W6 report, corrections report, review, validation and progress record the run. Step 9's plan was reviewed a second time at the user's direction, for grounding in the codebase, readability, value, and the bans on hard-coding, temporary patches, over-defense, redundancy and vague names. Eight findings (plan section 18.12): the DES has no "ready but unapplied" state, so the hook carries the completion hook's signature and no readiness classifier; the completion key names the scheduling iteration and is right only at PP=1, so the key rule applies to both observation kinds; the Replica's next forward id is the first existing identity to test, with its invariant-5 gap to be measured rather than assumed; the call site uses the constructor-required cluster scheduler without getattr/hasattr; the CPU oracle models the engine loop only and feeds the real balancer; names stay plain; validation code stays out of frontier/; DP engines are not iteration-lockstep in the reference. Records only; no Step 9 source change; execution still awaits the start signal.
…d from existing getters The room conjunct of the reference iteration is real and is what folds an admission that fills the pipeline into the following completion, which is also why PP=1 never publishes an admission on its own. The hook signature stays that of the completion hook; the policy reads num_running_batches and num_pipeline_stages itself. One mechanism for every PP.
…PP>1 The completion report Frontier publishes today is exact only when the engine has no batch queue. Above pipeline_parallel_size 1 an iteration can schedule new work, return without applying an older output, and still publish changed request counts, so a placement can depend on state Frontier never reports. reference_loop.py models that iteration and nothing else: the early-return conjunction from step_with_batch_queue, changed-count emission, and the per engine step counter. Emitted reports feed the existing VllmDPLoadBalancer, so the coordinator latch and the frontend score stay modeled in one place. The unit test pins the five reference states as executable expectations, including the two that no stride rule can express: a depth-three queue admits twice before the first completion, and both admissions publish.
P1(a) confirms the plan state table as written and shows that at depth one every iteration both schedules and applies, which is why the current completion-only report is already exact at PP=1. P1(b) probed three shapes and found the fourth unrunnable. MoE attn_dp=2 with num_pipeline_stages=2 drains the event queue with requests unfinished: stage admission mints a ticket per arriving batch and admits only the strict FIFO head, so a lane that admits num_pipeline_stages batches in one round places its peer behind a ticket belonging to a lane that is already busy. The three files involved are byte-identical to main, and nothing exercised the combination because every attn_dp>1 test uses one pipeline stage. The design checkpoint closes with the hook payload settled and the report key open. The ForwardSyncState forward id satisfies every invariant that the runnable shapes can test but collapses consecutive cold-fill admissions into one key, and the alternative that fixes that breaks peer equality. Deciding between them requires the shape that deadlocks.
…evidence The ground-truth checkout now carries the placement chain on its own branch, committed locally and not pushed. The case manifest records the checkout tuple, the diff artifact and its hash, the four record kinds with their join keys, and the two decisions that are still blocked. Package G1 needed one file fewer than planned: the scheduler needs no dp_rank column because the scheduler output already carries the scheduled request ids and the record is written by the engine that owns the rank.
W9-01 was fixed as a separate correctness item on fix/stage-admission-ordering (draft PR 36, rule dac4e69) and validated against vLLM DP=2/PP=2. Record the resolution, answer the case manifest's scope decision, and copy the branch summary and test report. Step 9's PP>1 packages stay paused until PR 36 is merged forward and G3b passes as the composition check with W3.
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.
Related to #26.
Scope
A small set of correctness fixes extracted from
bug/ttft-checkafter a source review, rebuilt against currentmainrather than merged wholesale.Latency calibration and any Frontier-versus-vLLM end-to-end latency comparison are out of scope. No accuracy claim is made by this PR. Issue #26 stays open. One scheduler-level comparison against vLLM is authorized as a later step (Step 9, DP placement under
PP>1); it compares placement decisions, not latency, and it has not started.Base
Stacked on #34, which brings the four modules this PR edits under the 2,000-line gate without changing behavior. The base here is
refactor/oversized-module-splitand will be retargeted tomainonce #34 merges, so the diff shown is only the behavior change.The reviewed parent changes were brought in by merge, not rebase, so no commit on this branch was discarded and the published review anchors stay valid. Current relationship: head
fix/issue26-correctness-pr, baserefactor/oversized-module-splitat2310417, merged in as0d025f8after the base corrected its fidelity gate's cache-eligibility rule (review finding C34-01).Progress
rtol=0, atol=0plus one FP8 structural check, the second run with the correctedblock_shapewiring; FP8 numerics not establishedW2, and how a behavior fix is measured
_schedule_batch_modedrew the replica index from a counter that persists across scheduling calls but drew the DP lane from the index within the current call, so lane assignment restarted at lane zero every time the scheduler was entered.mainalready applies the correct expression for the unified decode role, so this is an internal-consistency repair rather than a new policy.The fidelity matrix that gates #34 asserts nothing changes, which is the wrong question for a behavior fix. It was used differently here: the expected-to-move set was stated before measuring, so the result could falsify it.
Expected to move: the three online DP cases. Expected not to move: the two offline DP cases, where the whole request stream is admitted in one call and the two expressions already agree, and the other 66.
Measured: 71 of 71 compared, 68 identical, and the mismatch set was exactly the three predicted cases. Nothing else moved.
The direction was confirmed too, from the lane occupancy the stage ledger records:
The third row is the clearest evidence of the defect: replicas split evenly while every record sat on lane zero, one rotation working and the other not, in a single run.
Re-measured after the review
The first measurement ran the
6ab521dsource against the refactor tip's case table and comparator. The harness revision was disclosed in prose but not recorded as a field, and that harness could report success without having compared anything — which is what review comment R34-01 found and #34 now fixes.It was therefore repeated with the corrected harness, with source and harness at one revision: baseline
6ef0a3c, candidateceac2b4, both clean detached checkouts, one harness atceac2b4, no case filter, clean cache, 71 cases executed and 426 predictor cache files on each side.Every number above reproduces exactly, and the gate now also reports 0 baseline failures, 0 candidate-only failures, 0 cases with missing evidence, 0 cases with differing definitions, 0 cases not compared, and no provenance findings. The re-measurement changed the provenance of the evidence, not the evidence.
Comparison exit code is 1, and that is correct here: the gate reports inequality, while the acceptance criterion for a behavior fix is the stated expectation.
The candidate measured is
ceac2b4; up to the W2 checkpoint the later commits touched onlytests/andtask_memory/, andgit diff ceac2b4..HEAD -- frontier/was empty at that checkpoint, so these artifacts come from the W2 production tree. W3, W4, W6 and W7 changedfrontier/afterwards; their matrix runs are reported in their own sections.A coverage limit worth stating
The matrix validates the monolithic half of W2 only. The prefill role reaches the same placement path, but no shipped recipe can give it more than one lane: a dense model in a disaggregated architecture is rejected outright, and the MoE wrappers require
ATTN_TP == MOE_TP * MOE_EPwhile the runtime requiresattn_tp * attn_dp == moe_tp * moe_ep, which have no common solution above one lane. Both routes were tried and both were rejected, so this is measured rather than inferred.That is a limit of the shipped wrappers, not of the runtime, and the docstring recording it now says so. Prefill placement is covered at the scheduler level, through the public
schedule(); covering it through the real event loop needs a fixture that builds a runtime configuration directly with deterministic durations injected at the predictor boundary. That fixture now exists, as W3 acceptance work — see below.Four DP-placement cases were added to the matrix for this fix, since 66 of the previous 67 ran a single lane and could not see lane placement at all.
The placement tests, after review comment R35-01
The earlier tests compared one run against another. That shows placement does not depend on how the request stream is divided between calls, but it would also hold for a wrong rule applied consistently. And
test_replica_identity_contractread source text, which cannot tell a correct expression from an equivalent one it does not recognize.Three topologies now have their full rotation written out by hand, past its wraparound, for the case where both the replica and the lane advance — for example, replicas
[3, 11]with two lanes must produce(3,0) (11,0) (3,1) (11,1)repeating. The tests drive the publicschedule()rather than_schedule_batch_mode, so the cluster-type dispatch is exercised rather than assumed, and each runs for both roles that reach batch-mode placement,MONOLITHICandPREFILL. A guard keeps that role list honest:TRANSfalls through the same dispatch, and the list is only complete becauseTRANSis declared and never constructed anywhere infrontier/.The source-text check remains as a narrow governance check, with a docstring saying it does not establish behavior and must not be satisfied by rewording a correct expression.
What the negative control showed
Re-running these against the pre-fix method, taken verbatim from
6ab521d^and installed in memory, fails 12 of 23 and leaves the grouping control and the eight unrelated lane tests passing. The failures are more informative than the count: in all three topologies the old code produces the expected sequence exactly when the whole stream arrives in one call, and collapses onto lane 0 only when requests arrive one at a time.So the hand-derived expectation agrees with the rotation the code already performed for a single call. W2 did not introduce a placement policy; it made incremental arrival reach the placement that batch arrival already had. That is the strongest available statement that this is a bug fix, and it is why only the cases that enter the scheduler many times moved.
W3, and a defect the fidelity matrix cannot reach
A monolithic Replica runs prefill and decode on the same attention-DP lanes, so one forward step can hold a prefill batch on one lane and a pure-decode batch on another. Those two phases were given disjoint waiting rooms and disjoint open-step binding tables. Each room waits for
attn_dpmembers; neither reaches it; and the idle-lane filler cannot rescue either one, because each sibling stage is busy holding the other phase. The forward never dispatches.This needs more than one attention-DP lane, which is why no shipped recipe has ever hit it — and also why the 71-case matrix cannot prove anything about it. Same constraint as the W2 coverage limit above: the public MoE wrappers require
ATTN_TP == MOE_TP * MOE_EPwhile the runtime requiresattn_tp * attn_dp == moe_tp * moe_ep, and those have no common solution above one lane.So the primary evidence is the direct-construction fixture that R35-02 asked for. With
attn_tp=1, attn_dp=2, moe_tp=1, moe_ep=2on a monolithic MoE Replica, four requests of unequal length arriving together under chunked prefill, and the realSimulatorevent loop: on the pre-fix source the run drains its event queue with a non-empty scheduler state. That is the deadlock reproduced end to end, not argued from the source. On this branch the same configuration reaches 24 cohorts, 4 of them genuinely mixed-phase, and completes 4 of 4 requests.The fix is one lifecycle, not just a shared room
The plan forbids an intermediate state where the rooms are shared but completion is still phase-specific, so this lands as one commit:
MONOLITHICallocates one waiting room, bound to the forward name and to both phase names, and resolves its lanes in one"forward"step-id namespace.enter_layer_sync(..., mode), mirroring theschedule_layer_wave(mode=...)that already existed.forward_collective.pycompletes a cohort once: it advances the decode-phase requests once, restores the full-stage owners once, then runs each live source through the phase helper it already had, so every lane predicts its continuation from its own batch rather than from one sampled batch.One audit finding was narrowed by reading the source:
_next_step_id_by_replicais keyed by replica alone, so step-id allocation was already Replica-scoped and monotonic across kinds. Only the binding table and the waiting room were partitioned, and only those had to change.Event priority, which constrained the design
Events order by
(time, event_type, id), so the collective event class is a priority. Two things follow: a cohort shape that already works must keep the event type it has today, and the class must not depend on which lane happened to close the room — that would make a mixed cohort's ordering depend on arrival order.Both are satisfied by choosing the class from the cohort's contents, by the same rule the entry already uses: prefill if any live source carries prefill tokens, decode otherwise. A pure-prefill and a pure-decode cohort keep exactly the type they have now; only the mixed cohort, which could not complete at all, is new. No
EventTypevalue was added.One guard that delegation would have dropped
Each per-phase helper refuses a legacy aggregate synchronization by checking its batch for the wave's lane timings — but only when it pops the room itself. Delegation hands it
direct_batch, which skips that branch. The check now lives in the shared completion, once per source, against the marker its own phase writes. This was found while re-reading the delegation before measuring, and is covered by its own parametrized test.Controls, because a test that cannot fail proves nothing
The suite was run against four trees. Each carries the final source and the final tests and differs by exactly one edit, except the baseline tree, which carries the pre-fix parent in full. Each fails for its own reason:
RuntimeError: Sequential simulation ended with non-empty scheduler stateValueError: one attention-DP lane cannot occupy two open sync cohorts: replica=0, stage=0, lane=1, layer=1, sync_stage=pre_moeValueError: Decode post_moe layer counter cannot advance: request_id=1, completed_layer_count=4, total_layers=4ValueError: operation_id is already queued or active in this stage context: ('shared_layer', 0, 1, 0, 1, 'attention', 'FULL_STAGE_WORLD')The pre-fix column deserves one clarification, because "22 of 23 fail" could be read as the suite collapsing for an unrelated reason. It is not: the same-phase pairs pass every assertion about the forward itself — one shared identity, one collective event, two per-source continuations — and fail only at the last line, which inspects a shared room the pre-fix cluster has no structure for. The mixed pairs fail earlier, at an empty collective list. The gap between the two groups is the deadlock, isolated.
Selecting a source tree needed care, incidentally:
PYTHONPATHalone does not override an editable install, because its meta-path finder precedessys.path. An earlier control run silently tested the installed package and reported everything passing. The runner now strips that finder and asserts the resolved module path before collecting.Coverage is 23 behavior tests — every phase pairing including true mixed batches, both arrival orders, unequal source tokens, idle participation, duplicate ownership, successive layers, a decoding request carried inside a prefill batch, dense-layer transitions inside a MoE model, a missing wave marker, and a disabled metrics store — plus the real-runtime fixture, which additionally asserts token conservation, one completion per request, drained waiting rooms, no live stage ownership, and an identical run with metrics reporting on and off.
The matrix, and what a null result means
Baseline
3d47417against candidate65ed8a7, both clean detached checkouts withsource_dirty=Falseand no dirty paths, both driven by one harness revision, no filter, clean cache, 71 executed and 426 predictor cache files per side.71 of 71 compared, 71 identical, zero mismatches, zero provenance findings, zero predictor cache differences.
design.mdrecorded that expectation before the run, together with the one change that is reachable at a single lane: the decode-layer credit for an already-decoding request carried inside a prefill-mode batch. It predicted that credit would move nothing, because on the monolithic pathcompleted_layer_countfeeds only admission guards and diagnostics — its two arithmetic consumers are PD-AFDECODE_ATTN/M2N — and it predicted that if a MoE co-location case did move, that credit would be the cause. The primary prediction held and the conditional branch was not taken.Stated plainly, since the number invites the wrong reading: 71 of 71 identical is not evidence that the deadlock is fixed. No matrix case can reach a multi-lane monolithic MoE forward at all. The matrix answers only "did anything else move", and the answer is no.
A second defect, found by external review and fixed here
A dense layer in a MoE model completes per source batch, outside the shared forward completion that credits a routed layer to every decoding request in the cohort. The decode handler credited its own batch, but the prefill handler credited nothing, so a request that had finished its prefill and was carried in a prefill-mode batch (chunked prefill mixes them) missed every dense layer. On a
MoE -> dense -> MoEmodel its layer count read 1, 1, 2 instead of 1, 2, 3. On a monolithic Replica the undercount is silent; only the PDD decode role asserts the terminal count.The fix is one helper,
advance_decode_layer, holding the validate-then-credit rule once; the shared forward and decode handlers use it, and the dense-layer completion for a prefill-mode source credits that source's decoding members before delegating. A request still prefilling has no decode layer to credit, and a PREFILL-role batch carries none, so the disaggregated roles are unchanged.Evidence: a dense layer executed for a mixed source (+1 for the decoder, 0 for the prefiller, pure-prefill control credits nothing); the
MoE -> dense -> MoEsequence; and a real-loop hybrid-layer variant (moe_layers_enum="0,2,3") of the W3 runtime fixture that reads credits before the token rollout resets them: 4 mixed-source dense completions, 10 decode tokens credited, every one peaking at 4 of 4 layers. On the pre-fix source the same run peaks four of them at 3 of 4. Commitf7c31e4; reporttest_report_2026-09-22_review_corrections.md.Scope kept deliberately narrow
The candidate's decode final-metrics hunk calls a helper that
maindeleted. Rather than rewritemain's decode timing ownership inside a deadlock fix, it is excluded: introducing a second decode component ledger would move every reachable MoE decode case for a reason unrelated to the defect. The exclusion is recorded as a decision with its rationale indesign.md, not left as an oversight.W4, and a policy that has to prove it is not round-robin
Frontier routed requests to a Replica's attention-DP lanes by round-robin. No vLLM deployment does that. A vLLM V1 frontend picks an engine by scoring
waiting * 4 + runningover counts it only learns when the coordinator publishes a snapshot, so placement follows a delayed view of load, and the delay itself changes where a request lands.VllmDPLoadBalancermodels the two halves of that mechanism separately, because vLLM separates them:core_client.py:1139-1153): thewaiting * 4 + runningscore, the scan with a strict<so the lowest-index minimum wins, and the local increment of the chosen engine's waiting estimate, which is what spreads a burst of requests across lanes between two snapshots.coordinator.py:195-211): 100 ms while counts are changing, 5000 ms while idle, a 50 ms collection wait before the first snapshot, and the latch that publishes the previous step's counts before the current ones.Every constant carries its citation in the source. Out-of-order step reports warn and are still applied, as the reference does; nothing here aborts a run. The object creates no events, so a drained simulation still drains.
Off by default, and bounded in the constructor
It is reachable only through
--cluster_scheduler_config_type vllm_load_balancing.round_robinremains the default and nothing changes when the policy is not selected. Supported scope is enforced, not documented: one co-location Replica, one pipeline stage, thevllm_v1replica scheduler, and a report key whose ordering actually holds.That last guard is the interesting one, and it was settled by measurement rather than by argument. The report key is W3's shared forward identity. A probe at the report boundary showed:
attn_dp=2attn_dp=2, staggered online arrivalsattn_dp=1A MoE Replica resolves one shared forward identity across its lanes, which is exactly what W3 delivered. A dense Replica has no per-forward collective across lanes, so each lane keeps its own creation counter. The constructor therefore rejects dense above one lane and says why. No second step identity was invented to paper over it.
Two seams, and no mutable time bridge
Placement now depends on when it happens, so the time has to reach the policy honestly.
BaseClusterScheduler.schedule_at(time)defaults toschedule()andClusterScheduleEventcalls it; the policy overrides it, and its ownschedule()raises rather than routing from a stale snapshot.on_replica_batch_endis an inert default hook called afterreplica_scheduler.on_batch_end, so a policy reading lane populations there sees the post-step state.get_request_load()delegates to the existing decision-log waiting accessor, so a load balancer and the decision log cannot disagree about what "waiting" means.The runtime test that would catch a no-op
Each case runs one configuration twice — under the policy and under
round_robin— in one child process, with identical durations from the dummy predictor. The discriminating case spreads six arrivals over a second and gives the first request a 40-token decode:[0, 1, 1, 0, 1, 1][0, 1, 0, 1, 0, 1]Same arrivals, same durations, same lane capacity — and the policy puts strictly fewer requests on the lane still draining the long request. Round-robin cannot, because it cannot see load.
The same runs also record that routing times equal the
ClusterScheduleEventtimes at five distinct instants, that the report keys are non-decreasing with every equal-key pair carrying two distinct lanes, that every report matches the post-step load while strictly fewer match the pre-step load (6 of 10, 38 of 44, 3 of 7), and that no new event type appears.Controls, five trees and five distinct failure subsets
ClusterScheduleEventcallsschedule()againon_batch_endreports_after_the_lane_released_the_batchbecomes0 == 10WAITING_SCORE_WEIGHT = 1selectstops reserving the chosen lanedense_multi_laneconstruction caseThe unmodified control tree passes 64 of 64, so the harness is sound inside a control tree. One real defect surfaced while running the focused regression set and was fixed rather than explained away: the unit file builds both dense and MoE shapes, which trips the process-global
IS_MOElatch when it runs after another MoE configuration, so it now resets the simulation globals around each test.The matrix, and what a null result means here
71 of 71 cases identical, matching the expectation recorded in
design.mdbefore the run. Baselinecdfcdf5against candidate10dd474, both clean detached checkouts, one harness revision, clean cache, 426 predictor cache files each, zero provenance findings.Stated plainly: no matrix case selects the new policy. The matrix answers one question — did anything else move — and all three edits reachable from those paths are inert there. The policy's own evidence is the 61 unit tests, the three real-runtime cases and the controls above.
No placement or timing equivalence with a real vLLM deployment is claimed. IPC latency, more than one frontend, elastic scaling and the coordinator's warm-start phase are deliberately absent, and the report key is not a vLLM step counter — it advances per layer, and only its ordering and equality are used, which is all the reference coordinator uses its
(wave, step)pair for.W5, which is not in this PR, and why
The plan's fifth work package would have separated the MoE routing load distribution from the routing implementation used for timing prediction. It is closed without a source change, and the reasoning is worth stating because the audit originally recorded it as a reachable defect.
The collision needs two clusters in one run that resolve different routing implementations. They cannot.
moe_routing_distribution_typeis declared once, onReplicaConfig;ClusterConfigdeclares no{prefill,decode,decode_ffn}_replica_config_moe_routing_distribution_typefield, soget_field_value("moe_routing_distribution_type")(frontier/config/cluster_role_config.py:58-63) always takes its miss and returns the global value, and the generated CLI exposes exactly one flag for it. Verified on this branch and onmain. The audit had read theget_field_valuelookup as evidence of a declared override; that was wrong, andreview.mdnow carries the correction beside the original row rather than in place of it.So the fix becomes live code only after first adding the per-role override that makes the collision possible — and then a registry routing axis to defend against it. The current contract is kept instead: one global distribution, one derived routing path per run.
What the module decides is narrow: which
moe.csvrows trainmoe_gating_routing_topk, that is, whether that one operator's predicted cost comes from the fused-topk kernel or from the profiler's uniform round-robin routing path (frontier/profiling/moe/moe_impl.py:75-103,:195-225). It never touches expert load, grouped GEMM, EP synchronization or scheduling.Its magnitude was measured rather than assumed, on the three checked-in datasets that carry both paths. Matched-feature
moe_gating_routing_topkmedians, standard / uniform:a800/qwen3-a3b-30b-moe(259 matched rows)h800/Phi-tiny-MoE-instructh800/step-moe-noquant-smallThat is the cost of the existing mapping selecting the wrong kernel for a scenario, which is why the mapping stays. It is not a gain attributable to the reverted override: with the field unset every configuration resolved exactly as before, so its fidelity effect was zero by construction.
The drafted implementation — the global field, three per-role overrides, four CLI flags, a single-owner resolver, a routing-aware training signature and family gate, and a
(model_name, identity, routing_runtime_path)registry key with legacy-conflict rejection — was reverted before any commit and archived asw5_reverted_moe_routing_runtime_path.patchunder the task directory.W6, and a profiling measurement that was missing two kernels
_run_fused_moe_iterationis the profiling path Frontier takes whenever vLLM exposes its low-level fused-MoE API. It ran the first expert GEMM, then handedintermediate_cache1[:, :E]— the gate half of thegate | upprojection, unactivated — straight to the second GEMM, and never reduced the per-expert outputs. Two operations were absent: the gated activation and the local top-k reduction.Reachability was checked before writing any code
The W5 revert set a standard: establish that a defect is reachable from a released configuration, and measure its size, before adding anything. Both checks were run here first.
All five names the low-level branch imports resolve in the pinned reference vLLM v0.10.2, and
environment_profiling.ymlpinsvllm>=0.10,<0.11, so the documented profiling environment takes this path. Worth recording plainly: no environment on the development host reproduces it, because both local Torch environments carry newer vLLM and select the functionalfused_expertsbranch, which was always correct.The two missing kernels are memory-bound elementwise work, so their cost was estimated as
bytes / (peak HBM x 0.80)and compared against the measuredmoe_grouped_gemmmedians in the checked-in datasets:a800/qwen3-a3b-30b-moeh800/Qwen3-30B-A3B-tinyh800/step-moe-noquant-smallThe error scales with token count: negligible at decode batch sizes, material at prefill and chunked-prefill sizes. Well above the 0.5% bar, and the repair adds no configuration surface at all, so it was implemented.
The repair, and two deliberate departures from the recorded plan
torch.ops._C.silu_and_mulnow writes into a dedicated activation buffer, that buffer feeds the second GEMM and the FP8 quantizer, andmoe_sumreduces into a preallocated output. Both new buffers are allocated once at the call site, outside the timed step. Operand for operand this is vLLM's ownfused_experts_impl.No gated/non-gated branch was added.
profile_fused_moe_kernelonly ever materializesw1with2 * Erows, so a conditional would be unreachable code.Two adaptations recorded during the audit were not followed, and the reasons are in
review.mdrather than left implicit:_custom_opsimport went inside the low-leveltry, not outside it.fused_moe.pyimports_custom_opsat its own top, so it cannot fail where the low-level API succeeds and cannot perturb the two-branch detection. Placing it there additionally means a build lacking it selects the functional path instead of running an incomplete computation.SiluAndMulwrapper was not reused. Itsforwardcallstorch.emptyon every invocation, which would put an allocation inside the timed region and change whatmoe_grouped_gemmmeasures.What the operator now means
moe_grouped_gemmis the complete local expert computation: first GEMM, gated activation, optional activation quantization, second GEMM with the routing weights, and the local top-k reduction. That reduction is a per-token sum, not a collective, so no DP/TP/EP communication cost was added or removed.This converges the two backends rather than splitting them. vLLM's
fused_expertsreturns reduced hidden states, so the functional path always included both steps; the repair removes a scope disagreement that existed onmain.MOE_FAMILYhas no operator for the reduction, so counting it here counts it exactly once, with no fifth operator, no new column and no new trained model.Recorded because it differs: the Frontier-instrumented reference vLLM places
moe_sumoutside its ownrecord_function("moe_grouped_gemm")scope. That finer split is diagnostics; Frontier's operator of the same name means the whole local expert computation.Artifact identity: documented, not encoded
Nothing in a profiled row says which version produced it.
resolve_grouped_gemm_backendreturnsvllm_fusedfor both the low-level and the functional vLLM path, andprofiling_patch_tagturns out to hold three historical free-text values in one CSV with nothing in the source writing it. Nothing infrontier/reads the backend column; one test asserts the MXFP4 label.The maintainer's decision was to leave the metadata alone and state the limit instead.
docs/profiling/README.mdnow records what the operator measures, how large the pre-repair gap was, and that a row cannot be checked for completeness from its own columns — so the remedy is to re-profile, not to infer.CPU validation, and what it deliberately does not prove
tests/unit/test_moe_fused_expert_arithmetic.py, 7 tests, all passing. It replaces the native calls with plain-Torch references, so it validates the composition, not native numerics, and its docstring says so. It covers the full path against a written-out gated-SwiGLU reference, the call orderGEMM1 → silu_and_mul → GEMM2 → moe_sumwith routing weights on the correct GEMM, the reduction's shapes, the FP8 quantizer's input, workspace reuse across profiling steps, and the allocation-site dimensions of all four buffers under TP=2.One of those tests exists only to make the others falsifiable: it computes the old slice-only arithmetic and asserts the repaired result differs, so the reference-equality test cannot pass against the unrepaired path.
Regression was measured against a detached worktree at the pre-change
HEADover the same four existing files: 1 failed / 136 passed before, 1 failed / 143 passed after. The single failure is identical on both sides and needs an environment with Torch but without vLLM, which no local environment provides. The default-environment suite is unchanged at 84 failed / 3778 passed.No fidelity matrix, deliberately. The changed module requires Torch and the simulator environment has none, so the simulator cannot import it; the matrix consumes checked-in CSVs rather than fresh profiling. This repair changes what a future profiling run measures. It changes nothing about a simulation run from existing data.
Native parity, because a mock cannot settle this
tests/integration/test_moe_fused_expert_numerical_parity.pydrives the repaired iteration with the same buffer shapes, kernel config and block alignment thatprofile_fused_moe_kerneluses, then compares the output tensor against vLLM's ownfused_expertsatrtol=0, atol=0. Both sides run in one process on one GPU over identical activations, weights, routing weights, routing ids and expert map.The module skips unless CUDA is present and the build reports the low-level API, which is the only configuration that selects the repaired path.
It runs on an H800 worker under the official
vllm/vllm-openai:v0.10.2image, because no local environment selects that path. Result: 8 passed in 13.70 s — seven reference comparisons atrtol=0, atol=0and one FP8 structural check (gpu-h800-0110, torch 2.8.0+cu128, vLLM 0.10.2,VLLM_API_VERSION=0.10.x). The repaired profiling path reproduces vLLM'sfused_expertsoutput bit for bit on the production shapes, on the uneven-occupancy boundary case, and across repeated invocations.FP8 numerics remain unproven and are recorded as such rather than claimed: the FP8 case checks that the real kernels accept the gated activation and return finite output, not that the quantized arithmetic matches. Frontier quantizes with its own helpers, so a bit-exact FP8 comparison would first require matching those schemes.
The 2026-09-22 external review also found that the FP8 case passed
block_dimsbut notblock_shape, so the kernels, as run, read the block-quantized scales through their per-tensor path rather than the production invocationprofile_fused_moe_kerneluses. The test now forwardsblock_shape, and two CPU tests pin that both GEMM invocations receive it (ca1b9b6). The corrected FP8 case was re-run on an H800 undercodesign(exp-0922-202645-561899,gpu-h800-0095, same image and launcher): 8 passed in 14.27 s, exit 0, withblock_shape=[128, 64]reaching both GEMMs; it is still a structural check, so FP8 numerics remain unproven. Recorded in9df18d1. The same review corrected a scope claim: the low-level path aligns blocks before the timed step while vLLM's functional entry aligns insidefused_experts, so the two backends time different envelopes, which the profiling guide now states.W7, an empty collective that the backend refused
The specification-time uncertainty resolved in one direction first: the candidate's submodule commit
e564935dis unpublished, not inaccessible.fwyc0573/frontier-htsimis public and readable, its only branchmainis exactly the gitlink this branch pinned, that commit returns HTTP 422, and no local object store contains it. So the fix had to be written, not fetched.Three defects in the published runner, each confirmed by running it rather than by reading it:
tensor_bytes = 0and a deleted field produce the identicalmissing required fields: ['tensor_bytes'].-1passes validation and reaches flow generation.This is reachable from Frontier.
_validate_data_sizeaccepts zero, and the EP all-to-all payload isembedding_dim * 2 * routed_tokens, which is zero for a lane with no routed tokens in a step;predict_reduce_scatterfloor-divides by the device count and reaches zero the same way. Frontier cannot fix it alone, because a zero-byte collective still costs the intra-server synchronization latency — short-circuiting to0.0here would change the backend's semantics rather than accept the input.The repair, and where it lives
Entirely in the companion repository: fwyc0573/frontier-htsim#1, branch
fix/zero-payload-input-handling, commiteb7bc4f, also draft.tensor_bytesnow merges through the helper that treats onlyNoneand""as unset, instead of the one that reads0as missing on both sides. The required-field check became a table carrying, per field, whether zero is a legal value — zero stays "missing" forcollective_type,domain_dims,topology,nodes,gpus_per_serverandtp, where the argparse defaults genuinely use it to mean absent. A negative payload is rejected in the runner and again inScenario.validate(), so a serialized scenario cannot smuggle one past the schema. Nothing in flow generation, topology modelling or latency arithmetic changed.Frontier's side of this package is the gitlink and one test. No Frontier source file changes — which matches the candidate branch, whose own
collective_sim_cc_backend.pyis byte-identical to this one's.Two negative controls, because a gitlink bump is easy to mis-verify
tests/test_zero_payload_input.py, 9 testsmissing required fields: ['tensor_bytes']tests/unit/test_collective_sim_zero_payload.py, 4 testsb8518afThe Frontier tests use the canonical
TP=4 x DP=2, EP=8pod from AGENTS.md priced with the analytic NVLink model. An empty all-to-all and an empty reduce-scatter each keep the payload-independent7 x 0.5 us = 0.0035 mssynchronization term; a 1 MiB all-to-all costs0.0060486 ms; a negative payload still raises from Frontier's own guard. The module skips when the optional submodule is not initialized and built, which is the default.A fresh clone confirms the gitlink resolves from the published remote rather than from anything local: the module skips,
git submodule update --initchecks outeb7bc4ffrom GitHub,make -jbuilds, and the four tests pass.A governance scan the bump exposed
Initializing the submodule took
tests/unitfrom 84 failures to 85. Diffing theFAILEDlists named one:test_raw_model_profile_resolution_callsites_are_allowlistedast.parses every file underfrontier/, and the submodule adds 38 vendored files, one of which raisesIndentationErrorunder Python 3. Two sibling scans walked the same tree and tolerated it while silently measuring vendored code as Frontier's own.tests/frontier_sources.iter_frontier_sources()now yields the 413 Frontier-owned files and skips the vendored subtree; all three scans use it. The suite returns to 84 failed / 3782 passed, with an empty diff against the baselineFAILEDlist in both directions and the four extra passes accounted for by the new module.This was latent, not caused by the fix: it would have hit anyone who followed the AGENTS.md instruction to initialize the submodule.
One thing to carry into the merge
.gitmodulesstill recordsbranch = main, which is right for the state after the companion PR merges.git submodule update --init— the documented command — uses the recorded gitlink and lands oneb7bc4f.git submodule update --remotewould followmainand drop the fix. Re-point the gitlink atmainonce the companion PR merges.The combined regression, and what it does and does not prove
The suites were run together on the integrated branch rather than package by package, on
d881357, 32 commits ahead of the base.origin/mainwas re-fetched first and is still1f694f7, an ancestor of this head, so no integration merge was needed and none was made.pytest tests/unit -q --continue-on-collection-errorspytest tests/integration -q --continue-on-collection-errorsPP=2casesThe gate for the unit suite is the
FAILEDset, not the count. Diffed against the recordedorigin/mainbaseline list it is empty in both directions: nothing that passes on the base fails here, and no baseline failure was silently repaired. The 11 collection errors are modules that importtorchormatplotlib, which the simulator environment does not carry; they error identically on the base.On the integration side the one added skip is this branch's own W6 parity module, which skips without a GPU. All 5 errors are
test_pdaf_reference_lifecycle_observer.pyreporting the same absent pinned PD-AF Reference checkout; they are environmental and identical on the base.Pipeline coverage, because the new policy is PP1-only
VllmLoadBalancingClusterSchedulerrefuses anything but one pipeline stage, so it cannot reach the multi-stage path by itself. Lifting that guard is planned as Step 9 (PP>1placement checked against vLLM's engine-iteration state at the scheduler level, with a CPU reference-loop oracle); the plan was corrected against the 2026-09-22 external review and execution has not started. The cluster-scheduling, stage-dispatch, and metrics code it shares with everything else does run there, so fourPP=2runs cover it: co-location dense offline and online atTP=2, co-location MoE atAttn_TP=4, MoE_TP=2, MoE_EP=2, and sequential PDD dense with both roles atPP=2. Each script echoes its resolved topology and the logs confirm the intended values.The predictor cache, measured cold on purpose
The checked-in CSV smokes run against a repository
cache/that is already populated, so on their own they only ever measure the cache-hit path. The same simulation was run twice against an empty scratch cache through--metrics_config_cache_dir, leaving the repository cache untouched. The cold run took 26.9 s and wrote 63 predictor artifacts; the warm run took 2.1 s and wrote none; theirrequest_metrics.csvoutputs are byte-identical. The persisted-cache path therefore reproduces the freshly trained path exactly.What this does not prove
CPU only. No native profiling suite was run here, and no vLLM serving or TTFT comparison was performed — it is out of scope for this PR and is not needed to accept it. The PD-AF Reference-checkout tests could not run on this host. Apart from the two CSV smokes, the example runs use dummy execution time, so they validate structure, lifecycle, and conservation rather than latency accuracy.
One pre-existing defect found and deliberately not repaired
AGENTS.mdunder "Tests" namescomm_backend_tests/anddebug/, and tells a reader to start with two scripts undertests/debug/e2e-level/monolith_mode/scripts/. That tree exists neither on this branch nor onmain. The same missing tree is why 10 of the 84 baseline unit failures are intest_colocation_release_review_contracts.py, which resolves paths under it, and a docstring atfrontier/scheduler/replica_scheduler/vllm_v1_engine_replica_scheduler.py:16still points into it.This is one pre-existing defect class from the release scrub that removed
tests/debug/, unrelated to Issue 26. Repairing it properly means deciding whether the co-location review contracts should assert against shipped scripts or against a restored tree — a decision about the published test surface, not a documentation tweak. Making the doc read correctly while the contract test still fails on the same paths would hide the gap. It is reported infuture.mdand left alone; thePP=2coverage was taken through the example scripts instead.What the audit established
Every defect was confirmed against
mainat pinned revisions, not assumed from the candidate's commit messages. The vLLM reference was pinned and shown to be a direct descendant of upstream v0.10.2, with the files defining DP placement and count publication byte-identical to the tag.One audit finding was later corrected against the source:
forward_sync_statedoes not partition the step-id namespace by synchronization kind. Allocation is already Replica-scoped and monotonic; what is partitioned is the open-step binding table. The W3 defect is unchanged but the required change is narrower. The correction is recorded inreview.mdrather than quietly applied.Review material
Tracked under
task_memory/task_2026-09-21_issue26_correctness_pr/:plan.mdrequirements.mdprogress.mdreview.mdvalidation.mdfuture.mdtest_report_2026-09-22_review_corrections.mdreview.mdcarries the finding-by-finding dispositionsummary.mdPer-work-package reports sit alongside them, one per fix, plus the three pinned-source audit reports. Logs are not pasted into this description; the reports carry the commands and the numbers.
The implementation commits, as opposed to the record commits:
6ab521dfix(scheduler): keep round-robin DP rotation across scheduling calls, withceac2b4strengthening its tests after review65ed8a7fix(scheduler): give a monolithic Replica one shared forward across phases, withf7c31e4crediting decoding requests at a dense layer inside a mixed batch (review finding C35-01)10dd474feat(scheduler): add an opt-in vLLM-style DP request placement policyde2bee87269bacfix(profiling): complete the legacy fused-MoE expert computation,697f219its native parity test,79f599athe identity limits in the profiling guide,ca1b9b6the FP8block_shapewiring and the optional-torch skip (C35-02..04)1b95187fix(cc_backend): accept an empty collective through the collective-sim backend, withbeded3cnarrowing the governance scans the gitlink bump exposedd881357completes the cluster-scheduler list inAGENTS.mdStatus
Draft. Do not merge before review.
Technically complete, independently of GitHub's draft status: every work package is closed, the combined regression is recorded, and the final diff review is written up with its method stated — it was a self-review, not an independent one. W5 is closed without a source change. W6 landed with its native run passing 8 of 8 on an H800: seven reference comparisons at
rtol=0, atol=0plus one FP8 structural check; the FP8 wiring correction from the 2026-09-22 external review was re-run on an H800 and passed (8 of 8), still as a structural check. The external review's other findings against this PR (C35-01..05) are fixed and dispositioned inreview.md; its Step 9 plan corrections (P9-01..06) are applied to the plan only, a second plan review against the maintainer's core-module quality gates is recorded inplan.md§18.12 (9df18d1), and Step 9 has not started. W7 landed with its companion draft PR open at fwyc0573/frontier-htsim#1.Two items are carried forward in
future.md: re-point the collective-sim gitlink atmainonce the companion PR merges, and the pre-existingtests/debug/pointer defect described above.Merge order: the companion PR first, then re-point this branch's gitlink at its
main, then this PR. Issue #26 stays open.