agharsallah commited on
Commit
e334e95
Β·
1 Parent(s): 6a5dc7f

feat: Handle FP8 KV cache incompatibility with snapshot models in build command

Browse files
docs/adr/0031-fp8-quantization-control.md CHANGED
@@ -84,6 +84,18 @@ output quantized, we can pin `quantization="fp8"` on it in the catalogue.
84
  deploy (no stale-precision restores β€” the snapshot is keyed to the new function
85
  version). A model that can't serve FP8 fails at snapshot *creation*, which is the
86
  same loud no-healthy-container failure as the plain path.
 
 
 
 
 
 
 
 
 
 
 
 
87
  - Possible future unlock: FP8 weights halve the host-RAM needed for sleep level 1,
88
  which was the stated blocker for snapshotting `nemotron-3-nano-30b` (~60GB BF16,
89
  ADR-0030). Unverified β€” Nemotron-H may reject on-the-fly FP8 entirely β€” so this
 
84
  deploy (no stale-precision restores β€” the snapshot is keyed to the new function
85
  version). A model that can't serve FP8 fails at snapshot *creation*, which is the
86
  same loud no-healthy-container failure as the plain path.
87
+ - **FP8 KV cache is incompatible with sleep-mode/snapshot models on the pinned vLLM.**
88
+ `--kv-cache-dtype fp8` boots and snapshots fine, but the `/wake_up` path runs
89
+ `init_fp8_kv_scales()` over a post-sleep KV cache that is a *list* of per-layer
90
+ tensors (not one tensor), so `cache_tensor.zero_()` throws and every snapshot restore
91
+ 500s β€” an endpoint that boots but can never wake. This bit `nemotron-3-nano-4b`
92
+ (`gpu_snapshot=True`) under a global `MODAL_LLM_KV_CACHE_DTYPE=fp8` deploy.
93
+ `build_command` therefore **drops an FP8 `kv_cache_dtype` for any `gpu_snapshot`
94
+ model** and warns: snapshot is a structural per-model decision, the KV dtype a deploy
95
+ knob, so snapshot wins and the endpoint serves with full-precision KV cache. Weight
96
+ `--quantization fp8` is a different code path and is unaffected. To actually run FP8
97
+ KV cache on such a model, drop `gpu_snapshot` (trade the fast cold start for the KV
98
+ win) β€” or revisit once the vLLM pin advances past the bug.
99
  - Possible future unlock: FP8 weights halve the host-RAM needed for sleep level 1,
100
  which was the stated blocker for snapshotting `nemotron-3-nano-30b` (~60GB BF16,
101
  ADR-0030). Unverified β€” Nemotron-H may reject on-the-fly FP8 entirely β€” so this
modal/docs/deploying.md CHANGED
@@ -233,6 +233,14 @@ uv run scripts/deploy_modal.py nvidia --quantization none
233
  > `curl <url>/v1/models`); if a model won't start, redeploy that provider without
234
  > the flag. This is why all per-model defaults stay `None` for now. See ADR-0031.
235
 
 
 
 
 
 
 
 
 
236
  ## Auth
237
 
238
  Modal web endpoints are public by default. Secrets are supplied as environment
 
233
  > `curl <url>/v1/models`); if a model won't start, redeploy that provider without
234
  > the flag. This is why all per-model defaults stay `None` for now. See ADR-0031.
235
 
236
+ > **FP8 KV cache (`--kv-cache-dtype fp8`) is silently dropped for snapshot models.**
237
+ > On the pinned vLLM it crashes the `/wake_up` path (`init_fp8_kv_scales` β†’
238
+ > `'list' object has no attribute 'zero_'`), so an FP8-KV snapshot model boots but
239
+ > can never wake. `build_command` drops the flag for any `gpu_snapshot=True` model
240
+ > and logs a `⚠️` line at deploy; the endpoint serves with full-precision KV cache.
241
+ > FP8 *weights* (`--quantization fp8`) are unaffected. To run FP8 KV cache on such a
242
+ > model, set its `gpu_snapshot=False`. See ADR-0031.
243
+
244
  ## Auth
245
 
246
  Modal web endpoints are public by default. Secrets are supplied as environment
modal/service.py CHANGED
@@ -220,6 +220,21 @@ def build_command(cfg: ModelConfig) -> list[str]:
220
  if quantization:
221
  cmd += ["--quantization", quantization]
222
  kv_cache_dtype = _resolve_precision(KV_CACHE_DTYPE, cfg.kv_cache_dtype)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
223
  if kv_cache_dtype:
224
  cmd += ["--kv-cache-dtype", kv_cache_dtype]
225
  # Performance / throughput knobs (all data-driven from ModelConfig).
 
220
  if quantization:
221
  cmd += ["--quantization", quantization]
222
  kv_cache_dtype = _resolve_precision(KV_CACHE_DTYPE, cfg.kv_cache_dtype)
223
+ # FP8 KV cache is incompatible with sleep-mode/snapshot models on the pinned
224
+ # vLLM: the wake path runs init_fp8_kv_scales() over a post-sleep KV cache that
225
+ # is a *list* of per-layer tensors, not one tensor, so cache_tensor.zero_()
226
+ # throws and /wake_up 500s (every snapshot restore dies). Snapshot is a
227
+ # structural per-model decision; the KV dtype is a deploy knob β€” so snapshot
228
+ # wins. Drop the flag and warn loudly rather than ship an endpoint that boots
229
+ # but can never wake. Weight --quantization is unaffected (different code path).
230
+ if kv_cache_dtype and cfg.gpu_snapshot and kv_cache_dtype.lower().startswith("fp8"):
231
+ print(
232
+ f"⚠️ {cfg.endpoint_name}: dropping --kv-cache-dtype {kv_cache_dtype} β€” "
233
+ "FP8 KV cache crashes the snapshot wake path on the pinned vLLM (see ADR-0031). "
234
+ "Serving with full-precision KV cache. Drop gpu_snapshot to keep FP8 KV cache.",
235
+ flush=True,
236
+ )
237
+ kv_cache_dtype = None
238
  if kv_cache_dtype:
239
  cmd += ["--kv-cache-dtype", kv_cache_dtype]
240
  # Performance / throughput knobs (all data-driven from ModelConfig).
tests/test_modal_build_command.py CHANGED
@@ -85,6 +85,45 @@ def test_kv_cache_env_override(service, monkeypatch):
85
  assert cmd[cmd.index("--kv-cache-dtype") + 1] == "fp8"
86
 
87
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  # ── deploy script wiring ───────────────────────────────────────────────────────
89
 
90
 
 
85
  assert cmd[cmd.index("--kv-cache-dtype") + 1] == "fp8"
86
 
87
 
88
+ # ── FP8 KV cache Γ— snapshot incompatibility (vLLM wake-path crash) ─────────────
89
+
90
+
91
+ def test_fp8_kv_cache_dropped_for_snapshot_models(service):
92
+ # FP8 KV cache crashes the /wake_up path on snapshot models, so the flag is
93
+ # suppressed when gpu_snapshot is set β€” the endpoint serves with full-precision
94
+ # KV cache rather than booting into a state it can never wake from.
95
+ cmd = service.build_command(_make(service, kv_cache_dtype="fp8", gpu_snapshot=True))
96
+ assert "--kv-cache-dtype" not in cmd
97
+ # The snapshot flag itself still wins and is emitted.
98
+ assert "--enable-sleep-mode" in cmd
99
+
100
+
101
+ def test_fp8_kv_cache_env_override_dropped_for_snapshot_models(service, monkeypatch):
102
+ # The global deploy override is the common trigger: it lands on every model in
103
+ # the app, including snapshot ones, which must still drop it.
104
+ monkeypatch.setattr(service, "KV_CACHE_DTYPE", "fp8")
105
+ cmd = service.build_command(_make(service, gpu_snapshot=True))
106
+ assert "--kv-cache-dtype" not in cmd
107
+
108
+
109
+ def test_fp8_variant_kv_cache_dropped_for_snapshot_models(service):
110
+ # Every fp8 variant hits init_fp8_kv_scales, so fp8_e5m2 is dropped too.
111
+ cmd = service.build_command(_make(service, kv_cache_dtype="fp8_e5m2", gpu_snapshot=True))
112
+ assert "--kv-cache-dtype" not in cmd
113
+
114
+
115
+ def test_non_fp8_kv_cache_kept_for_snapshot_models(service):
116
+ # The guard only fires on fp8; a non-fp8 dtype passes through even with snapshot.
117
+ cmd = service.build_command(_make(service, kv_cache_dtype="auto", gpu_snapshot=True))
118
+ assert cmd[cmd.index("--kv-cache-dtype") + 1] == "auto"
119
+
120
+
121
+ def test_fp8_kv_cache_kept_for_non_snapshot_models(service):
122
+ # Without snapshot there's no wake path, so FP8 KV cache stays.
123
+ cmd = service.build_command(_make(service, kv_cache_dtype="fp8", gpu_snapshot=False))
124
+ assert cmd[cmd.index("--kv-cache-dtype") + 1] == "fp8"
125
+
126
+
127
  # ── deploy script wiring ───────────────────────────────────────────────────────
128
 
129