Skip to content

fix: capture-poisoned engine wedge + planner-driven KV pool sizing#875

Merged
kekzl merged 1 commit into
mainfrom
fix/capture-wedge-and-kv-pool
Jul 7, 2026
Merged

fix: capture-poisoned engine wedge + planner-driven KV pool sizing#875
kekzl merged 1 commit into
mainfrom
fix/capture-wedge-and-kv-pool

Conversation

@kekzl

@kekzl kekzl commented Jul 7, 2026

Copy link
Copy Markdown
Owner

Fixes #874.

Problem 1 — capture failure permanently wedged the server (#874)

Loading a GGUF Q*_K MoE model (Ornith-1.0-35B Q4_K_M, qwen3_5_moe) with default settings: the first prefill ≥ ~1700 tokens hit the 2048-chunk prefill graph capture, cuBLAS failed under capture (status 14, VRAM nearly exhausted), the MoE host-args guard threw — and the exception unwound past the open capture. cudaStreamEndCapture was never called, so the prefill stream stayed in capture state forever: every subsequent request failed with internal_error ("operation failed due to a previous error during capture") while /health kept reporting ok.

Fixes (defense in depth):

  • CudaGraphRunner::execute: try/catch around the captured forward — abort the capture (CudaGraphCapture::abort_capture()), mark capture_failed_, re-run eagerly. The request succeeds; capture is permanently disabled for that runner.
  • Engine::invalidate_graphs(): also invalidates the prefill runner and closes any stray open capture on the prefill stream (abort_stream_capture) — covers future mid-capture throw paths.
  • Scheduler gate: moe_prefill_uncapturable() — GGUF Q*_K MoE models (no CUTLASS NVFP4 grouped workspace) always take the legacy host-args MoE prefill, which can never be captured; don't attempt it (saves a wasted forward per chunk).
  • /health now returns 503 unhealthy once the worker has declared the CUDA context poisoned and stopped (previously hard-coded ok).

Problem 2 — KV pool ate all post-weight VRAM, weight caches built 0 tensors

compute_vram_budget reserved only the nvfp4 heuristic (~459 MiB) before letting the KV backstop fill all remaining VRAM. Q4_K sources are not nvfp4_beneficial, so the heuristic misses them entirely — but the StoragePlanner (source-aware) routes them to the FP16 cache (4088 MiB projected, logged as "diagnostic only (5.1.5)"). Result on Ornith-35B Q4_K_M: KV pool 9970 blocks / 6.2 GiB (~159k tokens nobody asked for), FP16/NVFP4 caches 0 tensors, decode 11 tok/s via on-the-fly dequant.

Fix: mirror the existing native-NVFP4 reserve pattern for GGUF sources — reserve the planner projection (plus phase reserve) before the KV backstop, gated on a 2× planner/heuristic divergence so tuned Q6_K/Q8 configs keep their pools unchanged, and capped so one full max_seq_len sequence always fits (concurrency stays bounded by scheduler admission).

Verification (local GPU, RTX 5090)

  • New tests: CaptureAbort.ThrowDuringCaptureLeavesStreamUsable (test-kv), VramBudgetReserve.* (test-e2e) — green; full make test-unit + make test-gpu green.
  • E2E Ornith-35B Q4_K_M, default settings (graphs ON): previously deterministic wedge on the long-context probe — now degen_suite 33 checks / 0 FAIL including full long-context, server stays healthy afterwards. Startup now shows planner-driven weight-cache reserve 5410.8 MiB, KV 30720→2048 blocks, caches build (fp16=132, nvfp4=27, nvfp4_moe=15 tensors). Decode 11 → 22 tok/s on the same probe.
  • Perf gate: decode −4.67% on Qwen3-8B-Q8 flagged as depressed-host by the gate itself (mem 10401 MHz / 342 W during bench, Q8_0 GGUF perf drift since 2026-05-29 baseline: tg128 259->238 (-8%), pp512 8260->7150 (-13%) #526 signature) and degraded to WARN; prefill +6.7%/+74% PASS. The graphs ON/OFF gate read 1.10× on this branch — cross-checked against the v0.16.2 release binary on the same host: 0.75×, i.e. host-state noise, not this diff. No intentional perf change to baseline configs (reservation is gated off for heuristic-covered sources — pinned by VramBudgetReserve.BeneficialSourcesKeepFullKvPool).

🤖 Generated with Claude Code

…zing

- CudaGraphRunner: abort capture + eager fallback when the forward throws
  mid-capture (stream was left capturing forever -> every later op failed)
- invalidate_graphs(): drop prefill runner + close stray captures (safety net)
- scheduler: skip prefill capture when MoE prefill is legacy host-args
- /health: report unhealthy (503) once the worker declared the context poisoned
- vram_budget: reserve the StoragePlanner projection for GGUF sources the
  nvfp4 heuristic can't see (Q4_K MoE built 0 cache tensors, 11 tok/s);
  KV pool keeps >= one full max_seq_len sequence

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@kekzl kekzl enabled auto-merge (squash) July 7, 2026 00:55
@kekzl kekzl merged commit b30160e into main Jul 7, 2026
7 checks passed
@kekzl kekzl deleted the fix/capture-wedge-and-kv-pool branch July 7, 2026 01:00
kekzl added a commit that referenced this pull request Jul 7, 2026
…loor, honest budget logs (#877)

Verified findings from the 2026-07-07 VRAM audit (docs/audit/vram_audit_2026_07_07.md):

- Delete dead GDNState module (init never called, gdn_state_ always null;
  GDN recurrent state runs through SSMState) incl. InferenceState fields
  and the never-read gdn_layer_map_.
- Delete DeviceAllocator/PinnedAllocator (zero runtime callers) and
  collapse the MemoryManager facade — Engine holds VRAMAllocator directly.
- Delete stranded plan_tier_of() wrapper and the hardcoded Phase-4b
  bisect constant with its dead branches.
- Gate moe raw_staging_buf on any_host_packed_experts (its only consumers
  are the host-expert branches of the legacy MoE forward) — no longer
  allocated on all-on-device loads.
- Hoist nvfp4_beneficial() to core/qtype.h as the single policy source
  (was re-derived as a lambda in vram_budget.cpp — G1 regression class).
- Add vram_reserve_floor() helper; convert the 6 true max(total/10,256MiB)
  sites and normalize the drifted 1 MiB floor in phase3_nvfp4_decode.
- Stop logging a phantom fp8= reservation when FP8 prefill is resolved
  off (sm_120 default); KV sizing math unchanged.
- Remove the vacuous !plan.failed guard on the #875 GGUF reserve branch
  (the budget-side plan runs unconstrained, failed is unreachable there)
  and document the deliberate two-plan design.
- Wire MemAccount notes for the weight-cache build totals and executor
  workspaces; header now states exactly which pools are attributed.

Perf-neutral by design: no hot-path changes; verify-fast tests green,
prefill gates pass, graphs on/off re-measured 2.58x on warmed clocks
(the scripted 1.26x run carried the depressed-host signature, #526).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Prefill graph capture under VRAM exhaustion: cuBLAS status 14 → poisoned capture → server permanently wedged (health still ok)

1 participant