Follow-up from #148 / PR #149, raised by an independent read-only review of that fix. Measurement first — no fix proposed.
The observation
#148's root cause was: the steel-hybrid prefill wraps every per-layer MLX temporary in a noCopy MTLBuffer (arrToBuf → asMTLBuffer); those wrappers are autoreleased, and the pool drains only at thread exit. On the lane path that thread is ContinuousScheduler's decode thread, so a whole prefill's wrappers stayed live at once:
retained Metal ≈ concurrency × promptLen × ~2.03MB/token
Fixed for lanes by draining per prefill chunk (PR #149).
That mechanism is path-agnostic. Tell.prefill (swift/Sources/QwispCore/TellRuntime.swift, the chunk loop) runs the same steel-hybrid forwardRowsHybrid at chunk 1024, on a per-request detached thread (LLMBackend.swift), with no per-chunk pool. Serialize's pool does drain at request end, so there is no cross-request ratchet — but the in-request peak should scale with prompt length by the same law.
Why this is not obviously already known
If the coefficient were the same, a 48K-token serialize prefill would imply ~100GB of simultaneous wrapper retention, which plainly does not happen — long serialize prefills work today. So either:
- the coefficient differs materially on that path (different call shape, fewer/smaller wrapped temporaries), or
- something already bounds it there, or
- nobody has ever watched
footprint during a long serialize prefill.
This contradiction is unresolved by reading alone and is exactly why this is a measurement issue, not a fix issue.
The measurement
One long single-stream prefill (e.g. 32–48K tokens) on the default serialize path, sampling footprint <pid> throughout (not ps rss — it undercounts wired GPU pages), plus MLX.Memory active/cache and MTLDevice.currentAllocatedSize if the QWISP_LANE_MEMDBG-style hook from PR #149 is generalised to that path.
Expected discriminator:
- footprint peak scales at ~MB per prompt token during prefill → same bug, quantify before fixing
- footprint stays flat / scales at KB per token → the lane path was special, close this
If it does reproduce
Note both natural fix sites (forwardRowsHybrid, Tell.prefill) are on the frozen forward path (CLAUDE.md prohibition 3), which is why PR #149 placed its drain lane-locally. A shared-point fix needs owner sign-off; a Tell.prefill-local drain mirroring PR #149 would be the lower-risk shape.
Refs
#148 (root cause + full measurement trail) · PR #149 (the lane fix) · TellRuntime.swift Tell.prefill · LLMBackend.swift (per-request thread) · SeedlessFusedVerify.swift arrToBuf / forwardRowsHybrid
Follow-up from #148 / PR #149, raised by an independent read-only review of that fix. Measurement first — no fix proposed.
The observation
#148's root cause was: the steel-hybrid prefill wraps every per-layer MLX temporary in a noCopy
MTLBuffer(arrToBuf→asMTLBuffer); those wrappers are autoreleased, and the pool drains only at thread exit. On the lane path that thread isContinuousScheduler's decode thread, so a whole prefill's wrappers stayed live at once:Fixed for lanes by draining per prefill chunk (PR #149).
That mechanism is path-agnostic.
Tell.prefill(swift/Sources/QwispCore/TellRuntime.swift, the chunk loop) runs the same steel-hybridforwardRowsHybridat chunk 1024, on a per-request detached thread (LLMBackend.swift), with no per-chunk pool. Serialize's pool does drain at request end, so there is no cross-request ratchet — but the in-request peak should scale with prompt length by the same law.Why this is not obviously already known
If the coefficient were the same, a 48K-token serialize prefill would imply ~100GB of simultaneous wrapper retention, which plainly does not happen — long serialize prefills work today. So either:
footprintduring a long serialize prefill.This contradiction is unresolved by reading alone and is exactly why this is a measurement issue, not a fix issue.
The measurement
One long single-stream prefill (e.g. 32–48K tokens) on the default serialize path, sampling
footprint <pid>throughout (notps rss— it undercounts wired GPU pages), plusMLX.Memoryactive/cache andMTLDevice.currentAllocatedSizeif theQWISP_LANE_MEMDBG-style hook from PR #149 is generalised to that path.Expected discriminator:
If it does reproduce
Note both natural fix sites (
forwardRowsHybrid,Tell.prefill) are on the frozen forward path (CLAUDE.md prohibition 3), which is why PR #149 placed its drain lane-locally. A shared-point fix needs owner sign-off; aTell.prefill-local drain mirroring PR #149 would be the lower-risk shape.Refs
#148 (root cause + full measurement trail) · PR #149 (the lane fix) ·
TellRuntime.swiftTell.prefill·LLMBackend.swift(per-request thread) ·SeedlessFusedVerify.swiftarrToBuf/forwardRowsHybrid