You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Blocked on #143. Do not start until P3 reports a bubble share above the decision threshold. This issue is the expensive option in the dispatch-count workstream and the only one that can silently break losslessness, so it is gated on measurement by design.
The observation
Every compute encoder in swift/Sources/QwispCore/ is created with makeComputeCommandEncoder() — the Metal default MTLDispatchType.serial. dispatchType and memoryBarrier do not appear anywhere in the module. So all ~654 dispatches/token (#143) are totally ordered, including pairs that are structurally independent.
Independent work that exists in the current op sequence and is currently serialized:
MoE block: the routed-expert chain (gqmm4_swiglu_rows → gather d), the shared-expert chain (gqmm4_swiglu_rows → shared down qmm), and the shared-gate qmm8 are three chains that only converge at shared_gate_combine_rows_fold.
GDN: z is produced by the in-proj demux and consumed only at gdn_norm_gate, so it is live across the whole conv/prep/recurrence chain; the recurrence itself (gated_delta_step, 32×Dv×Hv threads) is low-occupancy and has nothing to overlap with under the serial encoder.
Attn: the v-cache write is independent of q/k prep; only sdpa needs all three.
At M=1 every one of these kernels is a matvec that underutilizes the GPU on its own — which is exactly the regime where overlapping independent kernels, rather than making any single kernel faster, is the available lever.
The change
Switch the decode/verify forward encoders to .concurrent and insert memoryBarrier(scope:)only at true producer→consumer edges.
This is encode-order refactoring, not kernel surgery — no kernel source changes, no arithmetic changes. Prohibition 3 is satisfied in letter. It is nonetheless the highest-risk item in this workstream, for the reason below.
The risk, stated plainly
A missing barrier does not fail loudly. It produces a race whose outcome depends on scheduling, so it can pass RAWTESTS, pass a 512/512 lossless run, and then corrupt one token in production a week later. The usual "tests are green" evidence is not sufficient evidence here.
Consequence for how this must be built:
Write the dependency table first, as a reviewable artifact — every buffer in GdnScratch / AttnScratch / MoEScratch / hBuf / cache buffers, with its producer and consumer dispatches per layer type. The table is the deliverable that gets reviewed; the code is downstream of it. Ad-hoc "add barriers until it works" is the failure mode to avoid.
Start with the MoE block only, single layer, flag-gated. That is where the three independent chains are, it is the biggest share of the per-layer dispatch count, and it bounds the blast radius.
Gates
G-A (locked, model-free): concurrent vs serial bit-identical across a synthetic multi-layer stack, M ∈ {1, 2, 8}, both layer types, repeated ≥100× per configuration — a race that shows up once in 50 runs must be caught by the gate, not by a user. Bumps RAWTESTS total = N.
G-B (lossless, model): spec-vs-greedy 512/512 × 4 regimes, run more than once; flag-off byte-unchanged (QWISP_CONCURRENT_ENCODE=0 default, and it stays default-off until G-D and a full review round are both clean).
G-C (wiring): reviewer walks the dependency table against the encode order line by line, independently of the author. This is the load-bearing gate for this issue — treat it as such.
G-D (speed, driver-decided): tok/s and profLastGPUMs before/after on resident and streaming, plus the isolated MoE-block A/B from perf: Step 0 — decode dispatch inventory + serial-vs-concurrent encoder bubble probe #143 P3 as the sanity anchor. If the real-block win is materially below what P2's synthetic bound suggested, report the discrepancy rather than shipping on the synthetic number.
Honest priority note
Resident already decodes at 85–91 tok/s and the estimated roofline is ~250–300 tok/s (#143 U3, and that estimate is itself unconfirmed). If P4 shows the engine is nearer the roofline than the estimate suggests, or P3 shows the bubbles are small, this issue should be closed not-planned rather than attempted — the cheap wins in #144 and #145 do not depend on it, and a race introduced into the bit-proven decode core is a far worse outcome than leaving 10% on the table.
Refs
#143 (Step 0 — hard prerequisite) · #144, #145 (independent, cheaper dispatch-count reductions) · notes/01 §2-4 (the sync-floor framing, from the superseded MLX era) · SeedlessFusedForward.GPUScratch / MoEScratch / GdnScratch / AttnScratch (the aliasing surface) · #86, PR #70 (streaming-tier memory pressure, relevant to per-layer scratch)
Status
Blocked on #143. Do not start until P3 reports a bubble share above the decision threshold. This issue is the expensive option in the dispatch-count workstream and the only one that can silently break losslessness, so it is gated on measurement by design.
The observation
Every compute encoder in
swift/Sources/QwispCore/is created withmakeComputeCommandEncoder()— the Metal defaultMTLDispatchType.serial.dispatchTypeandmemoryBarrierdo not appear anywhere in the module. So all ~654 dispatches/token (#143) are totally ordered, including pairs that are structurally independent.Independent work that exists in the current op sequence and is currently serialized:
gqmm4_swiglu_rows→gather d), the shared-expert chain (gqmm4_swiglu_rows→shared down qmm), and the shared-gateqmm8are three chains that only converge atshared_gate_combine_rows_fold.zis produced by the in-proj demux and consumed only atgdn_norm_gate, so it is live across the whole conv/prep/recurrence chain; the recurrence itself (gated_delta_step, 32×Dv×Hv threads) is low-occupancy and has nothing to overlap with under the serial encoder.sdpaneeds all three.At M=1 every one of these kernels is a matvec that underutilizes the GPU on its own — which is exactly the regime where overlapping independent kernels, rather than making any single kernel faster, is the available lever.
The change
Switch the decode/verify forward encoders to
.concurrentand insertmemoryBarrier(scope:)only at true producer→consumer edges.This is encode-order refactoring, not kernel surgery — no kernel source changes, no arithmetic changes. Prohibition 3 is satisfied in letter. It is nonetheless the highest-risk item in this workstream, for the reason below.
The risk, stated plainly
A missing barrier does not fail loudly. It produces a race whose outcome depends on scheduling, so it can pass RAWTESTS, pass a 512/512 lossless run, and then corrupt one token in production a week later. The usual "tests are green" evidence is not sufficient evidence here.
Consequence for how this must be built:
GdnScratch/AttnScratch/MoEScratch/hBuf/ cache buffers, with its producer and consumer dispatches per layer type. The table is the deliverable that gets reviewed; the code is downstream of it. Ad-hoc "add barriers until it works" is the failure mode to avoid.GPUScratchcomment: 全層で再利用=serial 実行ゆえ安全). Under.concurrent, layer i+1's producer can legally start before layer i's consumer has read the same scratch slot. Either a barrier at every layer boundary (which caps the achievable win — worth estimating before implementing, since it may already fall below the perf: Step 0 — decode dispatch inventory + serial-vs-concurrent encoder bubble probe #143 threshold) or per-layer scratch (which costs memory, and on the streaming tier memory pressure is itself a known failure mode, cf. Requesting more testing with long context + swap thrashing regression #86 / PR fix(server): strict streaming right-sizes C to the machine's memory budget #70).Gates
total = N.QWISP_CONCURRENT_ENCODE=0default, and it stays default-off until G-D and a full review round are both clean).profLastGPUMsbefore/after on resident and streaming, plus the isolated MoE-block A/B from perf: Step 0 — decode dispatch inventory + serial-vs-concurrent encoder bubble probe #143 P3 as the sanity anchor. If the real-block win is materially below what P2's synthetic bound suggested, report the discrepancy rather than shipping on the synthetic number.Honest priority note
Resident already decodes at 85–91 tok/s and the estimated roofline is ~250–300 tok/s (#143 U3, and that estimate is itself unconfirmed). If P4 shows the engine is nearer the roofline than the estimate suggests, or P3 shows the bubbles are small, this issue should be closed not-planned rather than attempted — the cheap wins in #144 and #145 do not depend on it, and a race introduced into the bit-proven decode core is a far worse outcome than leaving 10% on the table.
Refs
#143 (Step 0 — hard prerequisite) · #144, #145 (independent, cheaper dispatch-count reductions) · notes/01 §2-4 (the sync-floor framing, from the superseded MLX era) ·
SeedlessFusedForward.GPUScratch/MoEScratch/GdnScratch/AttnScratch(the aliasing surface) · #86, PR #70 (streaming-tier memory pressure, relevant to per-layer scratch)