[DS V4.1][Engram] Support async prefetch for offloaded engram lookups and engram DP sharding (#56512)

Signed-off-by: Juntian Liu <Juntianl777@gmail.com> Signed-off-by: Woosuk Kwon <woosuk@inferact.ai> Co-authored-by: Roger Wang <hey@rogerw.io> Co-authored-by: ywang96 <> Co-authored-by: Woosuk Kwon <woosuk@inferact.ai> Co-authored-by: Codex <noreply@openai.com>

Juntian Liu committed Sep 13, 2026 at 02:44 UTC d2d649e674c75425d2d6975c87eb89fd4d55fff8
12 files changed +1820 -194
.buildkite/test_areas/distributed.yaml
+4
@@ -206,6 +206,9 @@ steps:
206 - tests/v1/distributed
207 - tests/v1/engine/test_engine_core_client.py
208 - tests/distributed/test_utils
209 + - tests/distributed/test_engram_dp_shard.py
210 + - vllm/config/engram.py
211 + - vllm/models/deepseek_v4_1/
212 commands:
213 # https://github.com/NVIDIA/nccl/issues/1838
214 - export NCCL_CUMEM_HOST_ENABLE=0
@@ -217,6 +220,7 @@ steps:
220 - TP_SIZE=1 DP_SIZE=4 pytest -v -s v1/distributed/test_hybrid_lb_dp.py
221 - pytest -v -s v1/engine/test_engine_core_client.py::test_kv_cache_events_dp
222 - pytest -v -s distributed/test_utils.py
223 + - pytest -v -s distributed/test_engram_dp_shard.py
224
225 - label: ":nvidia: (L4) Distributed Compile + Comm"
226 key: distributed-compile-comm-4-gpus
tests/distributed/test_engram_dp_shard.py new
+776
@@ -0,0 +1,776 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +"""Engram tables shared by co-located DP replicas.
4 +
5 +Four GPUs on one node split the hash heads four ways, whether as DEP
6 +(TP1 x DP4, the single-node MoE layout) or TP2 x DP2. A lookup only returns
7 +the right rows if the ids of every replica are gathered, each rank looks up
8 +its own heads, and each replica trades those tokens back for the heads its
9 +peers own.
10 +
11 +With shared host storage, DP replicas instead map the same TP slice and
12 +prefetch only their own tokens without DP gathers.
13 +"""
14 +
15 +from itertools import product
16 +from pathlib import Path
17 +from tempfile import TemporaryDirectory
18 +from threading import Event
19 +from types import SimpleNamespace
20 +
21 +import pytest
22 +import torch
23 +import torch.multiprocessing as mp
24 +
25 +from vllm.config import (
26 + EngramConfig,
27 + VllmConfig,
28 + get_current_vllm_config_or_none,
29 + set_current_vllm_config,
30 +)
31 +from vllm.config.load import LoadConfig
32 +from vllm.config.parallel import ParallelConfig
33 +from vllm.config.scheduler import SchedulerConfig
34 +from vllm.distributed import cleanup_dist_env_and_memory, parallel_state
35 +from vllm.distributed.parallel_state import (
36 + get_engram_dp_group,
37 + get_engram_dp_size,
38 + init_distributed_environment,
39 + initialize_model_parallel,
40 +)
41 +from vllm.forward_context import set_forward_context
42 +from vllm.models.deepseek_v4_1.common.engram import EngramLayout
43 +from vllm.models.deepseek_v4_1.nvidia import engram as engram_ops
44 +from vllm.models.deepseek_v4_1.nvidia.engram import (
45 + Engram,
46 + ParallelEngramEmbedding,
47 + engram_head_shard_rank,
48 + gather_engram_hashes,
49 +)
50 +from vllm.platforms import current_platform
51 +from vllm.utils.network_utils import get_open_port
52 +from vllm.utils.system_utils import update_environment_variables
53 +
54 +WORLD_SIZE = 4
55 +DIM, BLOCK = 64, 32
56 +# Prime-sized buckets, as the model's layout builds them.
57 +HEAD_SIZES = (97, 101, 103, 107, 109, 113, 127, 131)
58 +
59 +
60 +def _token_counts(dp_size: int) -> tuple[tuple[int, ...], ...]:
61 + """Even loads, then uneven ones with an idle replica to pad around."""
62 + return ((8,) * dp_size, (7, 0, 5, 1)[:dp_size], (7,) * dp_size)
63 +
64 +
65 +def _full_table(rows: int) -> tuple[torch.Tensor, torch.Tensor]:
66 + """The unsharded table, identical on every rank."""
67 + torch.manual_seed(0)
68 + weight = (torch.randn(rows, DIM) * 4).to(torch.float8_e4m3fn)
69 + scale_inv = torch.randint(120, 134, (rows, DIM // BLOCK), dtype=torch.uint8)
70 + return weight, scale_inv
71 +
72 +
73 +def _make_ids(head_sizes: tuple[int, ...], num_tokens: int, seed: int) -> torch.Tensor:
74 + """Ids of each head, drawn from that head's own bucket range."""
75 + torch.manual_seed(seed)
76 + offset = 0
77 + columns = []
78 + for size in head_sizes:
79 + columns.append(torch.randint(offset, offset + size, (num_tokens, 1)))
80 + offset += size
81 + return torch.cat(columns, dim=1).to(torch.int32).cuda()
82 +
83 +
84 +def _reference(weight, scale_inv, ids) -> torch.Tensor:
85 + """Dequantized lookup against the whole table."""
86 + valid = ids >= 0
87 + safe_ids = ids.clamp_min(0).long()
88 + values = torch.nn.functional.embedding(safe_ids, weight).float()
89 + scales = torch.nn.functional.embedding(safe_ids, scale_inv)
90 + scales = (scales.to(torch.int32) << 23).view(torch.float32)
91 + values = values.unflatten(-1, (-1, BLOCK)) * scales.unsqueeze(-1)
92 + return values.flatten(-2).to(torch.bfloat16).masked_fill(~valid.unsqueeze(-1), 0)
93 +
94 +
95 +@pytest.mark.parametrize(
96 + "architecture,options,edp_ranks,etp_ranks",
97 + [
98 + ("DeepseekV41ForCausalLM", {}, [0, 2], [0, 1]),
99 + ("Qwen4ExpForCausalLM", {}, None, [0, 1]),
100 + (
101 + "Qwen4ExpForCausalLM",
102 + {"embedding_across_dp": True},
103 + None,
104 + [0, 1, 2, 3],
105 + ),
106 + ("Qwen4ExpForConditionalGeneration", {}, None, [0, 1]),
107 + ],
108 +)
109 +def test_engram_group_creation_honors_model_and_options(
110 + monkeypatch, architecture, options, edp_ranks, etp_ranks
111 +):
112 + config = SimpleNamespace(
113 + model_config=SimpleNamespace(architecture=architecture, is_moe=False),
114 + parallel_config=ParallelConfig(
115 + tensor_parallel_size=2,
116 + data_parallel_size=2,
117 + distributed_executor_backend="mp",
118 + ),
119 + engram_config=EngramConfig(**options),
120 + )
121 + for name in (
122 + "_TP",
123 + "_ETP",
124 + "_ENGRAM_DP",
125 + "_DCP",
126 + "_PCP",
127 + "_PP",
128 + "_DP",
129 + "_EP",
130 + "_EPLB",
131 + ):
132 + monkeypatch.setattr(parallel_state, name, None)
133 + monkeypatch.setattr(parallel_state, "_WORLD", SimpleNamespace(local_rank=0))
134 + monkeypatch.setattr(torch.distributed, "is_initialized", lambda: True)
135 + monkeypatch.setattr(torch.distributed, "get_world_size", lambda: 4)
136 + monkeypatch.setattr(torch.distributed, "get_rank", lambda: 0)
137 + monkeypatch.setattr(parallel_state, "get_node_count", lambda: 1)
138 +
139 + def make_group(group_ranks, *args, **kwargs):
140 + ranks = next(ranks for ranks in group_ranks if 0 in ranks)
141 + return SimpleNamespace(ranks=ranks, rank_in_group=0, world_size=len(ranks))
142 +
143 + monkeypatch.setattr(parallel_state, "init_model_parallel_group", make_group)
144 + with set_current_vllm_config(config):
145 + initialize_model_parallel(tensor_model_parallel_size=2, backend="nccl")
146 + group = get_engram_dp_group()
147 + assert (group.ranks if group is not None else None) == edp_ranks
148 + assert parallel_state.get_etp_group().ranks == etp_ranks
149 +
150 +
151 +def _worker(rank: int, tp_size: int, port: int) -> None:
152 + torch.accelerator.set_device_index(rank)
153 + update_environment_variables(
154 + {
155 + "RANK": str(rank),
156 + "LOCAL_RANK": str(rank),
157 + "WORLD_SIZE": str(WORLD_SIZE),
158 + "MASTER_ADDR": "localhost",
159 + "MASTER_PORT": str(port),
160 + }
161 + )
162 + dp_size = WORLD_SIZE // tp_size
163 + dp_rank, tp_rank = divmod(rank, tp_size)
164 + vllm_config = VllmConfig(
165 + parallel_config=ParallelConfig(
166 + tensor_parallel_size=tp_size,
167 + data_parallel_size=dp_size,
168 + data_parallel_rank=dp_rank,
169 + ),
170 + scheduler_config=SchedulerConfig(
171 + max_model_len=8,
172 + is_encoder_decoder=False,
173 + max_num_batched_tokens=8,
174 + max_num_seqs=8,
175 + ),
176 + )
177 + # Supply the resolved Engram settings without constructing a full model.
178 + vllm_config.engram_config = EngramConfig()
179 + vllm_config.model_config = SimpleNamespace(
180 + architecture="DeepseekV41ForCausalLM", is_moe=True
181 + )
182 + try:
183 + init_distributed_environment()
184 + with set_current_vllm_config(vllm_config):
185 + initialize_model_parallel(tensor_model_parallel_size=tp_size)
186 + assert get_engram_dp_size() == dp_size
187 + assert get_engram_dp_group().ranks == [
188 + tp_rank + replica * tp_size for replica in range(dp_size)
189 + ]
190 + assert engram_head_shard_rank() == tp_rank * dp_size + dp_rank
191 +
192 + for failure in ("create", "open", "mmap", "register", "pinned", None):
193 + _check_shared_storage_lifetime_and_failures(failure)
194 +
195 + # Reuse the processes and communication groups across storage modes.
196 + for (cpu_offload, dp_shared_memory), n_heads, sequence_parallel in product(
197 + [(False, False), (True, False), (True, True)],
198 + [8, 7],
199 + [False, True] if tp_size > 1 else [False],
200 + ):
201 + _check_table(
202 + vllm_config,
203 + cpu_offload,
204 + dp_shared_memory,
205 + n_heads,
206 + sequence_parallel,
207 + )
208 + finally:
209 + cleanup_dist_env_and_memory()
210 +
211 +
212 +def _check_shared_storage_lifetime_and_failures(failure):
213 + """One peer's failure must roll back every mapping/registration, without hangs."""
214 + import gc
215 + import weakref
216 +
217 + group = get_engram_dp_group()
218 + runtime = torch.cuda.cudart()
219 + make_file = engram_ops.tempfile.NamedTemporaryFile
220 + map_file = engram_ops.mmap.mmap
221 + paths, mappings, registrations, unregistrations = [], [], [], []
222 + failed_rank = 0 if failure == "create" else 1
223 +
224 + def fail():
225 + raise OSError(f"injected {failure} failure")
226 +
227 + def temporary_file(*args, **kwargs):
228 + if failure == "create":
229 + fail()
230 + file = make_file(*args, **kwargs)
231 + paths.append(Path(file.name))
232 + return file
233 +
234 + def open_file(*args, **kwargs):
235 + if failure == "open" and group.rank_in_group == failed_rank:
236 + fail()
237 + return open(*args, **kwargs)
238 +
239 + def mapping(*args, **kwargs):
240 + if failure == "mmap" and group.rank_in_group == failed_rank:
241 + fail()
242 + result = map_file(*args, **kwargs)
243 + mappings.append(weakref.ref(result))
244 + return result
245 +
246 + def register(pointer, size, flags):
247 + if failure == "register" and group.rank_in_group == failed_rank:
248 + return SimpleNamespace(value=1)
249 + result = runtime.cudaHostRegister(pointer, size, flags)
250 + assert result.value == 0
251 + registrations.append(pointer)
252 + return result
253 +
254 + def unregister(pointer):
255 + unregistrations.append(pointer)
256 + result = runtime.cudaHostUnregister(pointer)
257 + assert result.value == 0
258 + return result
259 +
260 + with pytest.MonkeyPatch.context() as patch:
261 + patch.setattr(
262 + engram_ops,
263 + "tempfile",
264 + SimpleNamespace(NamedTemporaryFile=temporary_file),
265 + )
266 + patch.setattr(engram_ops, "open", open_file, raising=False)
267 + patch.setattr(
268 + engram_ops,
269 + "mmap",
270 + SimpleNamespace(mmap=mapping, MAP_SHARED=engram_ops.mmap.MAP_SHARED),
271 + )
272 + patch.setattr(
273 + torch.cuda,
274 + "cudart",
275 + lambda: SimpleNamespace(
276 + cudaHostRegister=register,
277 + cudaHostUnregister=unregister,
278 + ),
279 + )
280 + if failure == "pinned" and group.rank_in_group == failed_rank:
281 + patch.setattr(torch.Tensor, "is_pinned", lambda self: False)
282 + if failure is not None:
283 + stage = "cudaHostRegister" if failure in ("register", "pinned") else failure
284 + with pytest.raises(RuntimeError, match=f"EDP rank {failed_rank}.*{stage}"):
285 + engram_ops.DPSharedEngramStorage(128, DIM, BLOCK, group)
286 + assert unregistrations == registrations
287 + else:
288 + storage = engram_ops.DPSharedEngramStorage(128, DIM, BLOCK, group)
289 + assert all(not path.exists() for path in paths)
290 + storage_ref = weakref.ref(storage)
291 + parameter = torch.nn.Parameter(storage.weight, requires_grad=False)
292 + views = storage.get_views(parameter, storage.weight_scale_inv)
293 + del storage
294 + gc.collect()
295 + assert storage_ref() is None
296 + assert not unregistrations
297 + del parameter
298 + gc.collect()
299 + assert not unregistrations # UVA views still retain the CPU allocation.
300 + del views
301 + gc.collect()
302 + assert unregistrations == registrations
303 + for ref in mappings:
304 + mapped = ref()
305 + assert mapped is None or mapped.closed
306 + assert all(not path.exists() for path in paths)
307 +
308 +
309 +def _check_table(
310 + vllm_config, cpu_offload, dp_shared_memory, n_heads, sequence_parallel
311 +):
312 + parallel = vllm_config.parallel_config
313 + dp_size, tp_size = parallel.data_parallel_size, parallel.tensor_parallel_size
314 + dp_rank = parallel.data_parallel_rank
315 + tp_rank = engram_ops.get_tensor_model_parallel_rank()
316 + multithread = (
317 + dp_shared_memory and n_heads == 7 and sequence_parallel == (tp_size > 1)
318 + )
319 + load_config = (
320 + LoadConfig(
321 + load_format="safetensors",
322 + use_tqdm_on_load=False,
323 + model_loader_extra_config={
324 + "enable_multithread_load": True,
325 + "num_threads": 2,
326 + },
327 + )
328 + if multithread
329 + else vllm_config.load_config
330 + )
331 + with pytest.MonkeyPatch.context() as m:
332 + head_sizes = HEAD_SIZES[:n_heads]
333 + rows = sum(head_sizes)
334 + config = SimpleNamespace(
335 + hidden_size=DIM,
336 + hc_mult=2,
337 + rms_norm_eps=1e-6,
338 + engram_layer_ids=[0],
339 + engram_num_embeddings=[rows],
340 + engram_max_ngram_size=2,
341 + engram_n_heads=n_heads,
342 + engram_head_dim=DIM,
343 + engram_compressed_vocab_size=32,
344 + engram_pad_token_id=0,
345 + engram_vocab_size=HEAD_SIZES[0],
346 + )
347 + with set_current_vllm_config(vllm_config):
348 + weight, scale_inv = _full_table(rows)
349 + layout = EngramLayout(config)
350 + # Stub configuration inputs, not Engram's initialization or execution.
351 + component_config = SimpleNamespace(
352 + engram_config=EngramConfig(
353 + cpu_offload=cpu_offload,
354 + dp_shared_memory=dp_shared_memory,
355 + ),
356 + scheduler_config=vllm_config.scheduler_config,
357 + load_config=load_config,
358 + )
359 + with m.context() as init_patch, torch.device("cuda"):
360 + init_patch.setattr(
361 + engram_ops, "get_current_vllm_config", lambda: component_config
362 + )
363 + engram = Engram(
364 + config,
365 + quant_config=None,
366 + layout=layout,
367 + layer_hash_index=0,
368 + use_sequence_parallel=sequence_parallel,
369 + prefix="model.layers.0.engram",
370 + )
371 + layer = engram.embed_tokens
372 + if multithread:
373 + _load_shared_multithread(layer, weight, scale_inv, load_config)
374 + else:
375 + # Only the leader has valid checkpoint payload in shared mode.
376 + loader_weight = weight if not dp_shared_memory or dp_rank == 0 else None
377 + loader_scale = (
378 + scale_inv if not dp_shared_memory or dp_rank == 0 else None
379 + )
380 + layer.weight.weight_loader(layer.weight, loader_weight)
381 + layer.weight_scale_inv.weight_loader(
382 + layer.weight_scale_inv, loader_scale
383 + )
384 + if dp_shared_memory:
385 + assert layer.dp_size == 1
386 + assert layer.head_start == tp_rank * layer.part_n_hash_cols
387 + assert layer.weight.is_pinned() and layer.weight_scale_inv.is_pinned()
388 +
389 + def unexpected_collective(*args, **kwargs):
390 + raise AssertionError("shared lookup must not use DP collectives")
391 +
392 + m.setattr(get_engram_dp_group(), "all_gather", unexpected_collective)
393 +
394 + token_counts = _token_counts(dp_size)
395 +
396 + # Production forward runs after the construction-time config has exited.
397 + assert get_current_vllm_config_or_none() is None
398 + for batch_idx, counts in enumerate(token_counts):
399 + num_tokens = counts[dp_rank]
400 + # Ids differ per replica but must agree across its TP ranks.
401 + ids = _make_ids(head_sizes, num_tokens, seed=100 + dp_rank)
402 + if batch_idx == 2 and dp_rank == 1:
403 + ids.fill_(engram_ops.DEAD_ID)
404 + with set_forward_context(
405 + None,
406 + vllm_config,
407 + num_tokens=num_tokens,
408 + num_tokens_across_dp=torch.tensor(counts, dtype=torch.int32),
409 + ):
410 + gathered = gather_engram_hashes(
411 + ids, dp_shared_memory=layer.dp_shared_memory
412 + )
413 + expected_tokens = (
414 + num_tokens if dp_shared_memory else max(counts) * dp_size
415 + )
416 + assert gathered.shape[0] == expected_tokens
417 + engram.prepare_embeddings(gathered)
418 + looked_up = engram.embed(ids)
419 + standalone = layer(ids)
420 + expected = _reference(weight.cuda(), scale_inv.cuda(), ids)
421 + torch.testing.assert_close(standalone, expected, atol=0, rtol=0)
422 + if sequence_parallel:
423 + # SP hands back only this rank's token shard, zero-padded.
424 + chunk = -(-num_tokens // tp_size)
425 + expected = torch.nn.functional.pad(
426 + expected, (0, 0, 0, 0, 0, chunk * tp_size - num_tokens)
427 + )
428 + expected = expected[tp_rank * chunk : (tp_rank + 1) * chunk]
429 + torch.testing.assert_close(looked_up, expected, atol=0, rtol=0)
430 +
431 + if (
432 + cpu_offload
433 + and n_heads == 7
434 + and not sequence_parallel
435 + and (not dp_shared_memory or tp_size == 1)
436 + ):
437 + _check_dummy_hash_model_forward(
438 + vllm_config,
439 + engram,
440 + head_sizes,
441 + weight,
442 + scale_inv,
443 + dp_shared_memory,
444 + )
445 +
446 + replay = n_heads == 7 and (
447 + (dp_shared_memory and sequence_parallel == (tp_size > 1))
448 + or (
449 + cpu_offload
450 + and not dp_shared_memory
451 + and sequence_parallel == (tp_size > 1)
452 + )
453 + )
454 + if replay:
455 + captures = ("full", "breakable")
456 + for capture in captures:
457 + _check_prefetch_replay(
458 + engram,
459 + head_sizes,
460 + weight,
461 + scale_inv,
462 + dp_shared_memory,
463 + capture,
464 + )
465 + if dp_shared_memory and n_heads == 7 and sequence_parallel == (tp_size > 1):
466 + with m.context() as storage_patch:
467 + storage_patch.setattr(layer.weight, "data", layer.weight.data.clone())
468 + with pytest.raises(RuntimeError, match="storage must not be replaced"):
469 + layer(ids)
470 +
471 +
472 +def _check_dummy_hash_model_forward(
473 + vllm_config, engram, head_sizes, weight, scales, dp_shared_memory
474 +):
475 + """A metadata-less replica joins DP sharding but skips a shared lookup."""
476 + from vllm.models.deepseek_v4_1.common.engram import NgramHashState
477 + from vllm.models.deepseek_v4_1.nvidia import model as model_ops
478 +
479 + dp_rank = vllm_config.parallel_config.data_parallel_rank
480 + if dp_shared_memory and dp_rank != 1:
481 + return
482 + tokens = 4
483 + ids = _make_ids(head_sizes, tokens, seed=300 + dp_rank)
484 + state = NgramHashState.__new__(NgramHashState)
485 + torch.nn.Module.__init__(state)
486 + state.multipliers = torch.empty(1, 2, dtype=torch.int64, device="cuda")
487 + state.primes = torch.empty(1, 1, len(head_sizes), dtype=torch.int64, device="cuda")
488 + state.ensure_cache = lambda: True
489 + # Hash arithmetic is covered separately; keep the real model's branch and
490 + # NgramHashState.dummy_hashes, plus real preparation/lookup collectives.
491 + state.forward = lambda *args: ids.unsqueeze(1)
492 +
493 + class Decoder(SimpleNamespace):
494 + def __call__(self, hidden, positions, input_ids, *args):
495 + hashes, keep = args[-2:]
496 + if dp_shared_memory and dp_rank == 1:
497 + assert hashes is None and keep is None
498 + output = hidden.new_zeros(
499 + tokens, len(head_sizes), DIM, dtype=torch.bfloat16
500 + )
501 + else:
502 + assert hashes is not None and keep is not None
503 + assert bool(keep.all()) == (dp_rank != 1)
504 + if dp_rank == 1:
505 + assert torch.all(hashes == engram_ops.DEAD_ID)
506 + output = engram.embed(hashes[:, 0])
507 + return output, None, None, None, None
508 +
509 + model = SimpleNamespace(
510 + use_mega_moe=False,
511 + use_sequence_parallel=False,
512 + engram_hash=state,
513 + engram_swa_prefix="swa",
514 + engram_dp_shared_memory=dp_shared_memory,
515 + layers=[Decoder(engram=engram)],
516 + start_layer=0,
517 + end_layer=1,
518 + aux_hidden_state_layers=(),
519 + )
520 + metadata = (
521 + None
522 + if dp_rank == 1
523 + else {
524 + "swa": SimpleNamespace(
525 + query_start_loc=None, slot_mapping=None, block_table=None
526 + )
527 + }
528 + )
529 + with pytest.MonkeyPatch.context() as patch:
530 + patch.setattr(
531 + model_ops,
532 + "get_pp_group",
533 + lambda: SimpleNamespace(
534 + is_first_rank=True,
535 + is_last_rank=False,
536 + ),
537 + )
538 + patch.setattr(model_ops, "mhc_post_tilelang", lambda hidden, *args: hidden)
539 + with set_forward_context(
540 + metadata,
541 + vllm_config,
542 + num_tokens=tokens,
543 + num_tokens_across_dp=torch.full(
544 + (vllm_config.parallel_config.data_parallel_size,),
545 + tokens,
546 + dtype=torch.int32,
547 + ),
548 + ):
549 + output = model_ops.DeepseekV4Model.forward(
550 + model,
551 + torch.arange(tokens, device="cuda"),
552 + torch.arange(tokens, device="cuda"),
553 + intermediate_tensors=None,
554 + inputs_embeds=torch.zeros(tokens, DIM, device="cuda"),
555 + lookback_token_ids=torch.full((1, 1), -1, device="cuda"),
556 + )["hidden_states"]
557 + expected_ids = torch.full_like(ids, engram_ops.DEAD_ID) if dp_rank == 1 else ids
558 + expected = _reference(weight.cuda(), scales.cuda(), expected_ids)
559 + torch.testing.assert_close(output, expected, atol=0, rtol=0)
560 +
561 +
562 +def _load_shared_multithread(layer, weight, scale_inv, load_config):
563 + """Opposite shard completion orders must leave every shared parameter ready."""
564 + from safetensors.torch import save_file
565 +
566 + from vllm.model_executor.model_loader import weight_utils
567 + from vllm.model_executor.model_loader.default_loader import DefaultModelLoader
568 +
569 + tensors = {
570 + "weight": weight,
571 + "weight_scale_inv": scale_inv.view(torch.float8_e8m0fnu),
572 + }
573 + order = list(tensors)
574 + if get_engram_dp_group().rank_in_group % 2:
575 + order.reverse()
576 + first_loaded = Event()
577 + original_load = weight_utils.load_file
578 +
579 + def load_file(path, **kwargs):
580 + if Path(path).stem != order[0]:
581 + assert first_loaded.wait(timeout=30), "First shared weight load stalled"
582 + return original_load(path, **kwargs)
583 +
584 + with TemporaryDirectory() as directory, pytest.MonkeyPatch.context() as m:
585 + for name, tensor in tensors.items():
586 + save_file({name: tensor}, str(Path(directory) / f"{name}.safetensors"))
587 + m.setattr(weight_utils, "load_file", load_file)
588 + loader = DefaultModelLoader(load_config)
589 + source = DefaultModelLoader.Source(directory, revision=None)
590 + loaded = []
591 + for name, tensor in loader._get_weights_iterator(source):
592 + param = getattr(layer, name)
593 + param.weight_loader(param, tensor)
594 + loaded.append(name)
595 + first_loaded.set()
596 + assert loaded == order
597 +
598 + # Check before any further collective could mask incomplete shared writes.
599 + for name, tensor in tensors.items():
600 + param = getattr(layer, name)
601 + expected = tensor.narrow(0, param.engram_vocab_start, param.shape[0])
602 + torch.testing.assert_close(
603 + param.view(torch.uint8), expected.view(torch.uint8), atol=0, rtol=0
604 + )
605 +
606 +
607 +def _check_prefetch_replay(
608 + engram, head_sizes, weight, scale_inv, dp_shared_memory, capture
609 +):
610 + """DP-sharded and shared prefetches must replay with new IDs."""
611 + from vllm.compilation.breakable_cudagraph import BreakableCUDAGraphCapture
612 +
613 + ids = _make_ids(head_sizes, 8, seed=501)
614 + tp_rank = engram_ops.get_tensor_model_parallel_rank()
615 + tokens = 8 // engram.embed_tokens.tp_size if engram.use_sequence_parallel else 8
616 + output = torch.empty(
617 + tokens, len(head_sizes), DIM, device="cuda", dtype=torch.bfloat16
618 + )
619 +
620 + def step():
621 + gathered = gather_engram_hashes(ids.clone(), dp_shared_memory=dp_shared_memory)
622 + engram.prepare_embeddings(gathered)
623 + output.copy_(engram.embed(ids))
624 +
625 + with pytest.MonkeyPatch.context() as patch:
626 + patch.setattr(engram_ops, "engram_gathered_num_tokens", lambda: 8)
627 + warmup = torch.cuda.Stream()
628 + warmup.wait_stream(torch.cuda.current_stream())
629 + with torch.cuda.stream(warmup):
630 + step()
631 + if capture == "breakable":
632 + graph = BreakableCUDAGraphCapture()
633 + with torch.cuda.stream(warmup), graph:
634 + step()
635 + assert graph.num_eager_breaks == 2
636 + else:
637 + graph = torch.cuda.CUDAGraph()
638 + with torch.cuda.graph(graph, stream=warmup):
639 + step()
640 + torch.cuda.current_stream().wait_stream(warmup)
641 + for iteration in range(2):
642 + ids.copy_(_make_ids(head_sizes, 8, seed=600 + iteration))
643 + graph.replay()
644 + expected = _reference(weight.cuda(), scale_inv.cuda(), ids)
645 + if engram.use_sequence_parallel:
646 + expected = expected[tp_rank * tokens : (tp_rank + 1) * tokens]
647 + torch.testing.assert_close(output, expected, atol=0, rtol=0)
648 + torch.accelerator.synchronize()
649 +
650 +
651 +@pytest.mark.skipif(not current_platform.is_cuda(), reason="CUDA required")
652 +@pytest.mark.parametrize("tp_size", [1, 2])
653 +def test_engram_table_shared_across_dp_replicas(tp_size, monkeypatch):
654 + """Check all storage modes with one process launch per TP/DP topology."""
655 + if torch.accelerator.device_count() < WORLD_SIZE:
656 + pytest.skip(f"Need {WORLD_SIZE} GPUs to run the test.")
657 + monkeypatch.setenv("VLLM_USE_BREAKABLE_CUDAGRAPH", "1")
658 + mp.spawn(_worker, args=(tp_size, get_open_port()), nprocs=WORLD_SIZE)
659 + cleanup_dist_env_and_memory()
660 +
661 +
662 +@pytest.mark.parametrize(
663 + "cpu_offload,dp_size,error",
664 + [
665 + (False, 4, "requires cpu_offload=True"),
666 + (True, 1, "effective Engram DP size is 1"),
667 + ],
668 +)
669 +def test_engram_dp_shared_memory_runtime_requirements(
670 + monkeypatch, cpu_offload, dp_size, error
671 +):
672 + """Report unmet storage/topology requirements before allocating CUDA memory."""
673 + monkeypatch.setattr(engram_ops, "get_engram_dp_size", lambda: dp_size)
674 + with pytest.raises(ValueError, match=error):
675 + ParallelEngramEmbedding(
676 + sum(HEAD_SIZES),
677 + DIM,
678 + HEAD_SIZES,
679 + cpu_offload=cpu_offload,
680 + dp_shared_memory=True,
681 + )
682 +
683 +
684 +@pytest.mark.parametrize(
685 + "node_ids,dp_size,replica_size,expected",
686 + [
687 + ([0] * 4, 1, 4, 1),
688 + ([0] * 4, 4, 1, 4),
689 + ([0] * 4, 2, 2, 2),
690 + ([0] * 8 + [1] * 8, 8, 2, 4),
691 + ([0] * 6 + [1] * 6, 4, 1, 2),
692 + ([0] * 3 + [1] * 4, 7, 1, 1),
693 + ([0] * 8 + [1] * 8 + [2] * 8, 8, 3, 1),
694 + ([0, 1] * 4, 4, 2, 1),
695 + ([0] * 3 + [1] * 5, 4, 2, 1),
696 + ],
697 +)
698 +def test_engram_dp_shard_size_respects_node_boundaries(
699 + node_ids, dp_size, replica_size, expected, monkeypatch
700 +):
701 + """Sharing must fall back when replicas or node rank ranges do not align."""
702 + monkeypatch.setattr(parallel_state, "get_node_count", lambda: len(set(node_ids)))
703 + monkeypatch.setattr(
704 + parallel_state, "get_world_group", lambda: SimpleNamespace(cpu_group=None)
705 + )
706 + monkeypatch.setattr(
707 + parallel_state,
708 + "in_the_same_node_as",
709 + lambda group, src: [node == node_ids[src] for node in node_ids],
710 + )
711 + assert (
712 + parallel_state._engram_dp_shard_size(len(node_ids), dp_size, replica_size)
713 + == expected
714 + )
715 +
716 +
717 +@pytest.mark.parametrize("num_tokens", [0, 2, 4, 5])
718 +def test_engram_hash_padding_has_no_valid_rows(num_tokens, monkeypatch):
719 + """Idle/padded slots must not query row zero; oversized batches are rejected."""
720 + monkeypatch.setattr(engram_ops, "engram_gathered_num_tokens", lambda: 4)
721 + monkeypatch.setattr(
722 + engram_ops,
723 + "get_engram_dp_group",
724 + lambda: SimpleNamespace(all_gather=lambda ids, dim: torch.cat([ids, ids], dim)),
725 + )
726 + ids = torch.full((num_tokens, 2, 3), 17, dtype=torch.int32)
727 + if num_tokens > 4:
728 + with pytest.raises(ValueError, match="exceeds the DP token slot"):
729 + gather_engram_hashes(ids)
730 + return
731 + gathered = gather_engram_hashes(ids).reshape(2, 4, 2, 3)
732 + for replica in gathered:
733 + torch.testing.assert_close(replica[:num_tokens], ids)
734 + assert torch.all(replica[num_tokens:] == engram_ops.DEAD_ID)
735 +
736 +
737 +@pytest.mark.parametrize("tp_size", [1, 2])
738 +@pytest.mark.parametrize("uniform", [False, True])
739 +def test_engram_hash_padding_ignores_other_nodes(tp_size, uniform, monkeypatch):
740 + """Two four-GPU nodes pad only to their own maximum, including TP x DP."""
741 + group_size = 4 // tp_size
742 + counts = (
743 + [4] * (8 // tp_size)
744 + if uniform
745 + else [4096] + [2] * (group_size - 1) + [1, 3] + [0] * (group_size - 2)
746 + )
747 + monkeypatch.setattr(
748 + engram_ops,
749 + "get_forward_context",
750 + lambda: SimpleNamespace(
751 + dp_metadata=SimpleNamespace(num_tokens_across_dp_cpu=torch.tensor(counts))
752 + ),
753 + )
754 + for dp_rank in range(8 // tp_size):
755 + slot = 4 if uniform else (4096 if dp_rank < group_size else 3)
756 + ids = torch.full((counts[dp_rank], 2, 3), 17, dtype=torch.int32)
757 + monkeypatch.setattr(
758 + engram_ops,
759 + "get_dp_group",
760 + lambda rank=dp_rank: SimpleNamespace(rank_in_group=rank),
761 + )
762 + monkeypatch.setattr(
763 + engram_ops,
764 + "get_engram_dp_group",
765 + lambda rank=dp_rank: SimpleNamespace(
766 + rank_in_group=rank % group_size,
767 + world_size=group_size,
768 + all_gather=lambda tensor, dim: torch.cat([tensor] * group_size, dim),
769 + ),
770 + )
771 + gathered = gather_engram_hashes(ids)
772 + expected = ids.new_full((slot, 2, 3), engram_ops.DEAD_ID)
773 + expected[: ids.shape[0]] = ids
774 + torch.testing.assert_close(
775 + gathered, expected.repeat(group_size, 1, 1), msg=f"DP rank {dp_rank}"
776 + )
tests/engine/test_arg_utils.py
+22 -8
@@ -89,7 +89,10 @@ def test_watermark_config_cli():
89 @pytest.mark.parametrize(
90 "options",
91 [
92 - ["--engram-config", '{"cpu_offload": false, "embedding_across_dp": true}'],
92 + [
93 + "--engram-config",
94 + '{"cpu_offload": false, "embedding_across_dp": true}',
95 + ],
96 [
97 "--engram-config.cpu_offload",
98 "false",
@@ -98,9 +101,8 @@ def test_watermark_config_cli():
101 ],
102 ],
103 )
101 -def test_engram_config_cli(options, monkeypatch):
102 - """CLI settings take precedence over the legacy offload environment."""
103 - monkeypatch.setenv("VLLM_PLE_CPU_OFFLOAD", "1")
104 +def test_engram_config_cli(options):
105 + """JSON and dotted CLI options independently control Engram settings."""
106 parser = EngineArgs.add_cli_args(FlexibleArgumentParser())
107 args = EngineArgs.from_cli_args(parser.parse_args(options))
108 assert args.engram_config is not None
@@ -109,14 +111,26 @@ def test_engram_config_cli(options, monkeypatch):
111
112
113 @pytest.mark.parametrize(
112 - "options, provided",
113 - [([], False), (["--engram-config", "{}"], True)],
114 + "options,provided,dp_shared_memory",
115 + [
116 + ([], False, False),
117 + (["--engram-config", "{}"], True, False),
118 + (
119 + ["--engram-config", '{"dp_shared_memory": true}'],
120 + True,
121 + True,
122 + ),
123 + (["--engram-config.dp_shared_memory", "true"], True, True),
124 + ],
125 )
115 -def test_engram_config_cli_optional(options, provided):
116 - """An explicit empty config must remain distinct from an omitted config."""
126 +def test_engram_config_cli_optional(options, provided, dp_shared_memory):
127 + """Explicit configs honor defaults and the JSON/dotted DP shared-memory flag."""
128 parser = EngineArgs.add_cli_args(FlexibleArgumentParser())
129 args = EngineArgs.from_cli_args(parser.parse_args(options))
130 assert (args.engram_config is not None) == provided
131 + if provided:
132 + assert args.engram_config.cpu_offload is True
133 + assert args.engram_config.dp_shared_memory is dp_shared_memory
134
135
136 @pytest.mark.parametrize(
tests/kernels/test_engram.py
+226 -14
@@ -1,6 +1,7 @@
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 import bisect
4 +import inspect
5 from types import SimpleNamespace
6
7 import pytest
@@ -8,10 +9,13 @@ import torch
9
10 from vllm.models.deepseek_v4_1.common import engram as engram_ops
11 from vllm.models.deepseek_v4_1.common.engram import (
11 - Engram,
12 + Engram as CommonEngram,
13 +)
14 +from vllm.models.deepseek_v4_1.common.engram import (
15 NgramHashState,
13 - ParallelEngramEmbedding,
16 )
17 +from vllm.models.deepseek_v4_1.nvidia import engram as nvidia_engram_ops
18 +from vllm.models.deepseek_v4_1.nvidia.engram import Engram, ParallelEngramEmbedding
19 from vllm.platforms import current_platform
20
21
@@ -107,7 +111,7 @@ def test_fused_engram_post_wkv_matches_reference(
111 )
112 monkeypatch.setattr(engram_ops, "get_tensor_model_parallel_rank", lambda: tp_rank)
113
110 - module = Engram.__new__(Engram)
114 + module = CommonEngram.__new__(CommonEngram)
115 torch.nn.Module.__init__(module)
116 module.dim = dim
117 module.hc_mult = hc_mult
@@ -116,7 +120,7 @@ def test_fused_engram_post_wkv_matches_reference(
120 module.use_sequence_parallel = use_sequence_parallel
121 module.embed_tokens = torch.nn.Identity()
122 # `forward` reads rows staged by `prepare_embeddings`, so inject kv there.
119 - module.embed_tokens.tp_size = 1
123 + module.embed_tokens.tp_size = module.embed_tokens.n_hash_cols = 1
124 module.staged_rows = kv.unsqueeze(1)
125 if use_sequence_parallel:
126 padded = torch.nn.functional.pad(kv, (0, 0, 0, (-num_kv_tokens) % tp_size))
@@ -151,6 +155,25 @@ def _hash_state(use_slot_cache: bool) -> NgramHashState:
155 return state
156
157
158 +@pytest.mark.parametrize("num_tokens", [0, 7])
159 +def test_engram_dummy_hashes_leave_history_untouched(num_tokens):
160 + """Dummy forwards have no valid table IDs and do not alter cached history."""
161 + state = _hash_state(use_slot_cache=True)
162 + state.multipliers = torch.empty(2, 4, dtype=torch.int64)
163 + state.primes = torch.empty(2, 3, 8, dtype=torch.int64)
164 + state._cache = torch.arange(16, dtype=torch.int32)
165 + history = state._cache.clone()
166 +
167 + hashes, keep = state.dummy_hashes(torch.arange(num_tokens))
168 +
169 + assert hashes.shape == (num_tokens, 2, 24)
170 + assert hashes.dtype == torch.int32
171 + assert torch.all(hashes == engram_ops.DEAD_ID)
172 + assert keep.shape == (num_tokens,) and keep.dtype == torch.bool
173 + assert not keep.any()
174 + torch.testing.assert_close(state._cache, history)
175 +
176 +
177 @pytest.mark.parametrize("num_blocks", [4, 100])
178 def test_engram_cache_follows_kv_cache_binding(num_blocks):
179 """Discard profiling history when the real KV cache replaces its storage."""
@@ -533,9 +556,11 @@ def _make_embedding(cpu_offload, rows=4096, dim=256, block=32):
556 layer = ParallelEngramEmbedding.__new__(ParallelEngramEmbedding)
557 torch.nn.Module.__init__(layer)
558 layer.dim, layer.block_size, layer.tp_size = dim, block, 1
559 + layer.dp_size = 1
560 layer.n_hash_cols = layer.part_n_hash_cols = 24
561 layer.head_start = 0
562 layer.cpu_offload = cpu_offload
563 + layer.dp_shared_memory = False
564 layer.part_num_embeddings = rows
565 # A window strictly inside the table, so unowned rows are exercised too.
566 layer.vocab_start_idx, layer.vocab_end_idx = rows // 4, rows // 4 + rows // 2
@@ -559,12 +584,27 @@ def _make_embedding(cpu_offload, rows=4096, dim=256, block=32):
584 return layer
585
586
587 +@pytest.mark.parametrize(
588 + "tp_size,dp_size,n_heads", [(1, 4, 5), (2, 2, 5), (4, 1, 6), (8, 1, 6)]
589 +)
590 +def test_engram_rejects_empty_head_shards(tp_size, dp_size, n_heads, monkeypatch):
591 + """Reject empty owners before allocating weights or accessing CUDA."""
592 + monkeypatch.setattr(
593 + engram_ops, "get_tensor_model_parallel_world_size", lambda: tp_size
594 + )
595 + monkeypatch.setattr(nvidia_engram_ops, "get_engram_dp_size", lambda: dp_size)
596 + monkeypatch.setattr(engram_ops, "get_tensor_model_parallel_rank", lambda: 0)
597 + monkeypatch.setattr(nvidia_engram_ops, "get_tensor_model_parallel_rank", lambda: 0)
598 + with pytest.raises(AssertionError, match="ranks without hash heads"):
599 + ParallelEngramEmbedding(n_heads * 17, 64, (17,) * n_heads)
600 +
601 +
602 @pytest.mark.skipif(not current_platform.is_cuda(), reason="CUDA required")
603 @pytest.mark.parametrize("cpu_offload", [False, True])
604 @pytest.mark.parametrize("tp_size", [1, 2, 4, 8])
605 def test_engram_head_shards_reconstruct_checkpoint(cpu_offload, tp_size, monkeypatch):
606 """Keep complete buckets and reconstruct head order, including TP padding."""
567 - head_sizes = (17, 19, 23, 29, 31, 37)
607 + head_sizes = (17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73)
608 num_rows, dim = sum(head_sizes), 64
609 torch.manual_seed(0)
610 weight = torch.randn(num_rows + 7, dim).to(torch.float8_e4m3fn)
@@ -623,7 +663,7 @@ def test_engram_head_shards_reconstruct_checkpoint(cpu_offload, tp_size, monkeyp
663 module.staged_rows = torch.empty_like(shards[0])
664 module.prepare_embeddings(ids)
665 torch.testing.assert_close(module.embed(ids), expected, rtol=0, atol=0)
626 - # Slicing before head reordering must preserve padded heads and empty owners.
666 + # Slicing before head reordering must preserve padded heads and tokens.
667 module.use_sequence_parallel = True
668 chunk = (len(ids) + tp_size - 1) // tp_size
669 padded = torch.nn.functional.pad(expected, (0, 0, 0, 0, 0, (-len(ids)) % tp_size))
@@ -636,6 +676,31 @@ def test_engram_head_shards_reconstruct_checkpoint(cpu_offload, tp_size, monkeyp
676 )
677
678
679 +@pytest.mark.skipif(not current_platform.is_cuda(), reason="CUDA required")
680 +def test_engram_lookup_reuses_jit_across_token_shapes():
681 + """Runtime token counts and launch grids must share one JIT variant."""
682 + layer = _make_embedding(cpu_offload=False)
683 + kernel = engram_ops._engram_lookup_kernel
684 + kernel_cache = kernel.device_caches[torch.accelerator.current_device_index()][0]
685 + kernel_cache.clear()
686 +
687 + cache_sizes = []
688 + for background in (False, True):
689 + for num_tokens in (1, 7, 256):
690 + ids = torch.zeros(num_tokens, 24, dtype=torch.int32, device="cuda")
691 + out = torch.empty(
692 + num_tokens,
693 + 24,
694 + layer.dim,
695 + dtype=torch.bfloat16,
696 + device="cuda",
697 + )
698 + layer.lookup(ids, out, background=background)
699 + cache_sizes.append(len(kernel_cache))
700 +
701 + assert cache_sizes == [1] * len(cache_sizes)
702 +
703 +
704 @pytest.mark.skipif(not current_platform.is_cuda(), reason="CUDA required")
705 @pytest.mark.parametrize("cpu_offload", [False, True])
706 @pytest.mark.parametrize("background", [False, True])
@@ -660,19 +725,147 @@ def test_engram_lookup_matches_torch(cpu_offload, background, num_tokens):
725
726
727 @pytest.mark.skipif(not current_platform.is_cuda(), reason="CUDA required")
663 -@pytest.mark.parametrize("cpu_offload", [False, True])
728 +@pytest.mark.parametrize(
729 + "backend,cpu_offload", [("nvidia", None), ("nvidia", False), ("common", False)]
730 +)
731 +def test_engram_constructor_honors_offload(monkeypatch, backend, cpu_offload):
732 + """Default offload uses pinned storage and a side stream; False uses HBM."""
733 + from vllm.config import EngramConfig
734 +
735 + config = SimpleNamespace(hidden_size=16, hc_mult=1, rms_norm_eps=1e-6)
736 + layout = SimpleNamespace(
737 + num_embeddings=(72,),
738 + head_dim=32,
739 + max_ngram_size=2,
740 + n_heads=24,
741 + primes=(((3,) * 24,),),
742 + )
743 + vllm_config = SimpleNamespace(
744 + engram_config=EngramConfig()
745 + if cpu_offload is None
746 + else EngramConfig(cpu_offload=cpu_offload),
747 + scheduler_config=SimpleNamespace(max_num_batched_tokens=8),
748 + )
749 + offloaded = backend == "nvidia" and vllm_config.engram_config.cpu_offload
750 + if backend == "common":
751 + vllm_config.engram_config = None
752 + monkeypatch.setattr(engram_ops, "get_current_vllm_config", lambda: vllm_config)
753 + monkeypatch.setattr(
754 + nvidia_engram_ops, "get_current_vllm_config", lambda: vllm_config
755 + )
756 + monkeypatch.setattr(engram_ops, "get_tensor_model_parallel_world_size", lambda: 1)
757 + monkeypatch.setattr(engram_ops, "get_tensor_model_parallel_rank", lambda: 0)
758 + monkeypatch.setattr(
759 + engram_ops, "ReplicatedLinear", lambda *a, **k: torch.nn.Identity()
760 + )
761 + with torch.device("cuda"):
762 + cls = Engram if backend == "nvidia" else CommonEngram
763 + module = cls(config, None, layout, 0, False, "engram")
764 + layer = module.embed_tokens
765 + if offloaded:
766 + assert layer.weight.device.type == "cpu" and layer.weight.is_pinned()
767 + else:
768 + assert layer.weight.device.type == "cuda"
769 + layer.weight.data.copy_(torch.randn(layer.weight.shape).to(torch.float8_e4m3fn))
770 + layer.weight_scale_inv.data.fill_(127)
771 + streams = []
772 + lookup = layer.lookup
773 +
774 + def track_lookup(ids, out, background=False):
775 + streams.append(torch.cuda.current_stream())
776 + lookup(ids, out, background=background)
777 +
778 + layer.lookup = track_lookup
779 + main = torch.cuda.current_stream()
780 + ids = torch.randint(
781 + layer.vocab_start_idx,
782 + layer.vocab_end_idx,
783 + (8, 24),
784 + device="cuda",
785 + dtype=torch.int32,
786 + )
787 + module.prepare_embeddings(ids)
788 + expected = _reference_lookup(
789 + layer.weight.cuda(),
790 + layer.weight_scale_inv.cuda(),
791 + ids,
792 + layer.vocab_start_idx,
793 + layer.vocab_end_idx,
794 + )
795 + torch.testing.assert_close(module.embed(ids), expected, atol=0, rtol=0)
796 + if offloaded:
797 + assert streams == [module._prefetch_stream]
798 + assert streams[0] != main
799 + else:
800 + assert streams == [main]
801 +
802 +
803 +@pytest.mark.skipif(not current_platform.is_cuda(), reason="CUDA required")
804 +@pytest.mark.parametrize(
805 + "cpu_offload,delay",
806 + [(False, None), (True, None), (True, "producer"), (True, "lookup")],
807 +)
808 @pytest.mark.parametrize("capture", ["eager", "full", "breakable"])
665 -def test_engram_prepared_rows_survive_graph_breaks(cpu_offload, capture):
666 - """Consume early lookup results after a break, with fresh IDs each replay."""
667 - _run_engram_prepared_rows(cpu_offload, capture)
809 +def test_engram_prepared_rows_survive_graph_breaks(cpu_offload, capture, delay):
810 + """Temporary lookup IDs survive allocator reuse and graph replay."""
811 + _run_engram_prepared_rows(cpu_offload, capture, delay=delay)
812 +
813 +
814 +@pytest.mark.skipif(not current_platform.is_cuda(), reason="CUDA required")
815 +@pytest.mark.parametrize(
816 + "missing_dependency", ["record_stream", "producer_wait", "consumer_wait"]
817 +)
818 +def test_engram_prefetch_detects_missing_dependency(monkeypatch, missing_dependency):
819 + """Negative controls must expose incorrect rows when a dependency is removed."""
820 + if (
821 + missing_dependency == "record_stream"
822 + and torch.cuda.memory.get_allocator_backend() != "native"
823 + ):
824 + pytest.skip(
825 + "The allocator reuse negative control requires the native allocator"
826 + )
827 +
828 + def start(self, hash_ids, rows, stream):
829 + if missing_dependency != "producer_wait":
830 + stream.wait_stream(torch.cuda.current_stream())
831 + if missing_dependency != "record_stream":
832 + hash_ids.record_stream(stream)
833 + with torch.cuda.stream(stream):
834 + self.embed_tokens.lookup(hash_ids, rows, background=True)
835 +
836 + def finish(self, stream):
837 + pass
838 +
839 + monkeypatch.setattr(Engram, "_start_prefetch", start)
840 + if missing_dependency == "consumer_wait":
841 + monkeypatch.setattr(Engram, "_finish_prefetch", finish)
842 + delay = "producer" if missing_dependency == "producer_wait" else "lookup"
843 + try:
844 + with pytest.raises(
845 + AssertionError, match=r"Tensor-likes are not (?:equal|close)!"
846 + ):
847 + _run_engram_prepared_rows(True, "eager", delay=delay)
848 + finally:
849 + torch.accelerator.synchronize()
850
851
852 def _run_engram_prepared_rows(
671 - cpu_offload, capture, tp_size=1, rank=0, use_sequence_parallel=False
853 + cpu_offload, capture, tp_size=1, rank=0, use_sequence_parallel=False, delay=None
854 ):
673 - from vllm.compilation.breakable_cudagraph import BreakableCUDAGraphCapture
855 + from vllm.compilation.breakable_cudagraph import (
856 + BreakableCUDAGraphCapture,
857 + eager_break_during_capture,
858 + )
859
860 layer = _make_embedding(cpu_offload)
861 + if delay == "lookup":
862 + lookup = layer.lookup
863 +
864 + def delayed_lookup(indices, out, background=False):
865 + torch.cuda._sleep(2_000_000)
866 + lookup(indices, out, background=background)
867 +
868 + layer.lookup = delayed_lookup
869 cols, num_tokens = (23, 65) if use_sequence_parallel else (24, 64)
870 layer.n_hash_cols = cols
871 layer.tp_size = tp_size
@@ -681,6 +874,17 @@ def _run_engram_prepared_rows(
874 engram = Engram.__new__(Engram)
875 torch.nn.Module.__init__(engram)
876 engram.embed_tokens = layer
877 + engram._prefetch_stream = torch.cuda.Stream() if cpu_offload else None
878 + # Exercise the production eager boundaries even when the test process
879 + # imported Engram before breakable graphs were enabled.
880 + if capture == "breakable":
881 + import vllm.envs as envs
882 +
883 + with pytest.MonkeyPatch.context() as patch:
884 + patch.setattr(envs, "VLLM_USE_BREAKABLE_CUDAGRAPH", True)
885 + for name in ("_start_prefetch", "_finish_prefetch"):
886 + fn = eager_break_during_capture(inspect.unwrap(getattr(Engram, name)))
887 + setattr(engram, name, fn.__get__(engram, Engram))
888 engram.use_sequence_parallel = use_sequence_parallel
889 engram.staged_rows = torch.empty(
890 num_tokens,
@@ -709,7 +913,12 @@ def _run_engram_prepared_rows(
913 embed = torch.compile(embed, backend="eager", fullgraph=True, dynamic=True)
914
915 def step(cap=None):
712 - engram.prepare_embeddings(src)
916 + if delay == "producer":
917 + torch.cuda._sleep(2_000_000)
918 + # Drop the last reference to a non-contiguous input after launching lookup.
919 + engram.prepare_embeddings(hashes.clone()[:, 1])
920 + # Exercise same-size allocator reuse before consuming the prefetched rows.
921 + torch.empty_like(hashes).fill_(layer.part_num_embeddings - 1)
922 if cap is not None:
923 cap.add_eager(lambda: None)
924 out.copy_(embed(src))
@@ -730,9 +939,12 @@ def _run_engram_prepared_rows(
939 with torch.cuda.stream(warmup), graph:
940 step(graph)
941 torch.cuda.current_stream().wait_stream(warmup)
733 - assert graph.num_graphs == 2
942 + assert graph.num_eager_breaks == (3 if cpu_offload else 1)
943
944 for _ in range(3):
945 + # Eager prefill can overwrite staging rows between decode replays.
946 + hashes.random_(0, layer.part_num_embeddings)
947 + step()
948 hashes.random_(0, layer.part_num_embeddings)
949 if graph is None:
950 step()
tests/test_config.py
+110 -48
@@ -1158,31 +1158,68 @@ def test_engram_tensor_parallel_size(dp_size: int, across_dp: bool, expected: in
1158 assert config.get_parallel_size(parallel) == expected
1159
1160
1161 -def test_engram_rejects_elastic_cross_dp():
1161 +@pytest.mark.parametrize("option", ["embedding_across_dp", "dp_shared_memory"])
1162 +def test_engram_rejects_elastic_cross_dp(option):
1163 parallel = ParallelConfig(
1164 tensor_parallel_size=4,
1165 data_parallel_size=2,
1166 distributed_executor_backend="mp",
1167 )
1168 parallel.enable_elastic_ep = True
1168 - with pytest.raises(ValueError, match="embedding_across_dp.*elastic EP"):
1169 - EngramConfig(embedding_across_dp=True).verify_parallel_config(parallel)
1169 + with pytest.raises(ValueError, match=f"{option}.*elastic EP"):
1170 + EngramConfig(**{option: True}).verify_parallel_config(parallel)
1171
1172
1172 -@pytest.mark.parametrize("legacy", [None, "0", "1"])
1173 -def test_engram_cpu_offload_environment_fallback(monkeypatch, legacy):
1174 - """Explicit settings must override the legacy environment fallback."""
1175 - monkeypatch.delenv("VLLM_PLE_CPU_OFFLOAD", raising=False)
1176 - if legacy is not None:
1177 - monkeypatch.setenv("VLLM_PLE_CPU_OFFLOAD", legacy)
1178 - assert EngramConfig().cpu_offload == (legacy == "1")
1179 - assert EngramConfig(cpu_offload=False).cpu_offload is False
1180 - assert EngramConfig(cpu_offload=True).cpu_offload is True
1173 +def test_engram_dp_shared_memory_requires_cpu_offload():
1174 + with pytest.raises(ValueError, match="requires cpu_offload"):
1175 + EngramConfig(cpu_offload=False, dp_shared_memory=True)
1176 +
1177 +
1178 +@pytest.mark.parametrize(
1179 + "dp_size,load_format,multithread,error",
1180 + [
1181 + (1, "auto", False, "requires data_parallel_size > 1"),
1182 + (2, "dummy", False, "requires load_format"),
1183 + (2, "sharded_state", False, "requires load_format"),
1184 + (2, "auto", False, None),
1185 + (2, "safetensors", True, None),
1186 + (2, "pt", True, None),
1187 + ],
1188 +)
1189 +def test_engram_dp_shared_memory_config_validation(
1190 + monkeypatch, dp_size, load_format, multithread, error
1191 +):
1192 + """Reject invalid shared configs before distributed init; allow threaded loads."""
1193 + monkeypatch.setattr(current_platform, "is_cuda", lambda: True)
1194 + config = cast(
1195 + VllmConfig,
1196 + SimpleNamespace(
1197 + model_config=SimpleNamespace(
1198 + architecture="DeepseekV41ForCausalLM",
1199 + hf_text_config=SimpleNamespace(engram_layer_ids=[1]),
1200 + ),
1201 + speculative_config=None,
1202 + engram_config=EngramConfig(cpu_offload=True, dp_shared_memory=True),
1203 + parallel_config=ParallelConfig(data_parallel_size=dp_size),
1204 + load_config=LoadConfig(
1205 + load_format=load_format,
1206 + model_loader_extra_config={"enable_multithread_load": multithread},
1207 + ),
1208 + ),
1209 + )
1210 + if error:
1211 + with pytest.raises(ValueError, match=error):
1212 + VllmConfig._resolve_and_verify_engram_config(config)
1213 + else:
1214 + VllmConfig._resolve_and_verify_engram_config(config)
1215
1216
1217 @pytest.mark.parametrize(
1218 "architecture, ple_layers, cuda, supported",
1219 [
1220 + ("DeepseekV41ForCausalLM", [1], True, True),
1221 + ("DeepseekV41ForCausalLM", [], True, False),
1222 + ("DeepseekV41ForCausalLM", [1], False, False),
1223 ("Qwen4ExpForCausalLM", [1], True, True),
1224 ("Qwen4ExpForConditionalGeneration", [1], True, True),
1225 ("Qwen4ExpForCausalLM", [], True, False),
@@ -1201,7 +1238,9 @@ def test_engram_model_support(monkeypatch, architecture, ple_layers, cuda, suppo
1238 ModelConfig,
1239 SimpleNamespace(
1240 architecture=architecture,
1204 - hf_text_config=SimpleNamespace(ple_layer_ids=ple_layers),
1241 + hf_text_config=SimpleNamespace(
1242 + ple_layer_ids=ple_layers, engram_layer_ids=ple_layers
1243 + ),
1244 ),
1245 )
1246 if architecture is not None
@@ -1214,55 +1253,73 @@ def test_engram_model_support(monkeypatch, architecture, ple_layers, cuda, suppo
1253 with pytest.raises(ValueError, match="requires a model with supported Engram"):
1254 config.verify_model_config(model)
1255
1256 + resolved = cast(
1257 + VllmConfig,
1258 + SimpleNamespace(
1259 + model_config=model,
1260 + speculative_config=None,
1261 + parallel_config=ParallelConfig(),
1262 + load_config=LoadConfig(load_format="dummy"),
1263 + engram_config=None,
1264 + ),
1265 + )
1266 + VllmConfig._resolve_and_verify_engram_config(resolved)
1267 + assert (resolved.engram_config is not None) == supported
1268 + if supported:
1269 + assert resolved.engram_config.cpu_offload is True
1270
1218 -def test_engram_config_defaults_to_none(monkeypatch):
1271 +
1272 +@pytest.mark.parametrize(
1273 + ("value", "expected"), [(None, True), ("0", False), ("1", True)]
1274 +)
1275 +def test_engram_cpu_offload_environment_default(monkeypatch, value, expected):
1276 monkeypatch.delenv("VLLM_PLE_CPU_OFFLOAD", raising=False)
1277 + if value is not None:
1278 + monkeypatch.setenv("VLLM_PLE_CPU_OFFLOAD", value)
1279 + assert EngramConfig().cpu_offload is expected
1280 + assert EngramConfig(cpu_offload=False).cpu_offload is False
1281 + assert EngramConfig(cpu_offload=True).cpu_offload is True
1282 +
1283 +
1284 +def test_engram_config_defaults_to_none():
1285 config = VllmConfig()
1286 assert config.engram_config is None
1287 assert config.compute_hash()
1288
1289
1225 -@pytest.mark.parametrize("legacy", ["0", "1"])
1226 -def test_engram_none_resolves_legacy_offload(monkeypatch, legacy):
1227 - """Legacy enablement materializes a config before model validation."""
1228 - monkeypatch.setenv("VLLM_PLE_CPU_OFFLOAD", legacy)
1229 - monkeypatch.setattr(current_platform, "is_cuda", lambda: True)
1290 +@pytest.mark.parametrize("explicit", [False, True])
1291 +@pytest.mark.parametrize("enable_dbo,ubatch_size", [(True, 0), (False, 2)])
1292 +def test_deepseek_engram_rejects_microbatching(explicit, enable_dbo, ubatch_size):
1293 + """Reject overlapping Engram staging even without an explicit EngramConfig."""
1294 config = cast(
1295 VllmConfig,
1296 SimpleNamespace(
1297 model_config=SimpleNamespace(
1234 - architecture="Qwen4ExpForCausalLM",
1235 - hf_text_config=SimpleNamespace(ple_layer_ids=[1]),
1298 + architecture="DeepseekV41ForCausalLM",
1299 + hf_text_config=SimpleNamespace(engram_layer_ids=[1]),
1300 ),
1301 speculative_config=None,
1238 - engram_config=None,
1239 - parallel_config=ParallelConfig(),
1302 + engram_config=EngramConfig(cpu_offload=False) if explicit else None,
1303 + parallel_config=ParallelConfig(
1304 + enable_dbo=enable_dbo, ubatch_size=ubatch_size
1305 + ),
1306 ),
1307 )
1242 - VllmConfig._resolve_and_verify_engram_config(config)
1243 - if legacy == "1":
1244 - assert config.engram_config is not None
1245 - assert config.engram_config.cpu_offload is True
1246 - else:
1247 - assert config.engram_config is None
1308 + with pytest.raises(
1309 + ValueError, match="Engram does not support DBO or microbatching"
1310 + ):
1311 + VllmConfig._resolve_and_verify_engram_config(config)
1312
1313
1250 -@pytest.mark.parametrize("legacy", ["0", "1"])
1251 -def test_engram_explicit_config_requires_supported_model(monkeypatch, legacy):
1314 +def test_engram_explicit_config_requires_supported_model():
1315 """Explicit all-false settings still opt into model validation."""
1253 - monkeypatch.setenv("VLLM_PLE_CPU_OFFLOAD", legacy)
1316 with pytest.raises(ValueError, match="requires a model with supported Engram"):
1317 VllmConfig(engram_config=EngramConfig(cpu_offload=False))
1318
1319
1258 -def test_engram_legacy_offload_requires_supported_model(monkeypatch):
1259 - monkeypatch.setenv("VLLM_PLE_CPU_OFFLOAD", "1")
1260 - with pytest.raises(ValueError, match="requires a model with supported Engram"):
1261 - VllmConfig()
1262 -
1263 -
1320 @pytest.mark.parametrize("target_has_ple", [False, True])
1265 -def test_engram_draft_config_validates_target(monkeypatch, target_has_ple):
1321 +@pytest.mark.parametrize("explicit", [False, True])
1322 +def test_engram_draft_config_validates_target(monkeypatch, target_has_ple, explicit):
1323 """MTP may inherit cross-DP sharding without having its own PLE layers."""
1324 monkeypatch.setattr(current_platform, "is_cuda", lambda: True)
1325 target = SimpleNamespace(
@@ -1277,24 +1334,29 @@ def test_engram_draft_config_validates_target(monkeypatch, target_has_ple):
1334 speculative_config=SimpleNamespace(
1335 draft_model_config=draft, target_model_config=target
1336 ),
1280 - engram_config=EngramConfig(embedding_across_dp=True),
1337 + engram_config=EngramConfig(embedding_across_dp=True) if explicit else None,
1338 parallel_config=ParallelConfig(),
1339 + load_config=LoadConfig(load_format="dummy"),
1340 ),
1341 )
1284 - if target_has_ple:
1285 - VllmConfig._resolve_and_verify_engram_config(config)
1286 - else:
1342 + if explicit and not target_has_ple:
1343 with pytest.raises(ValueError, match="requires a model with supported Engram"):
1344 VllmConfig._resolve_and_verify_engram_config(config)
1345 + else:
1346 + VllmConfig._resolve_and_verify_engram_config(config)
1347 + assert (config.engram_config is not None) == target_has_ple
1348 + if target_has_ple:
1349 + assert config.engram_config.cpu_offload is True
1350
1351
1291 -def test_engram_hash_tracks_storage_and_sharding():
1352 +def test_engram_hash_tracks_execution_options():
1353 configs = [
1293 - EngramConfig(cpu_offload=offload, embedding_across_dp=across_dp)
1294 - for offload in (False, True)
1295 - for across_dp in (False, True)
1354 + EngramConfig(cpu_offload=True),
1355 + EngramConfig(cpu_offload=False),
1356 + EngramConfig(cpu_offload=True, embedding_across_dp=True),
1357 + EngramConfig(cpu_offload=True, dp_shared_memory=True),
1358 ]
1297 - assert len({config.compute_hash() for config in configs}) == 4
1359 + assert len({config.compute_hash() for config in configs}) == len(configs)
1360
1361
1362 @pytest.mark.parametrize("port", [1, 29550, 65535])
vllm/config/engram.py
+45 -15
@@ -3,18 +3,17 @@
3
4 from typing import TYPE_CHECKING
5
6 -from pydantic import Field
6 +from pydantic import Field, model_validator
7 +from typing_extensions import Self
8
9 import vllm.envs as envs
10 from vllm.config.utils import config, get_hash_factors, hash_factors
10 -from vllm.logger import init_logger
11
12 if TYPE_CHECKING:
13 + from vllm.config.load import LoadConfig
14 from vllm.config.model import ModelConfig
15 from vllm.config.parallel import ParallelConfig
16
16 -logger = init_logger(__name__)
17 -
17 # Architecture -> the hf_text_config field naming its n-gram layers. A model is
18 # only configurable here if it actually has such layers to store.
19 _NGRAM_LAYER_FIELDS = {
@@ -25,28 +24,44 @@ _NGRAM_LAYER_FIELDS = {
24
25
26 def _default_cpu_offload() -> bool:
28 - """Honor the legacy environment variable only when the field is omitted."""
29 - if envs.is_set("VLLM_PLE_CPU_OFFLOAD"):
30 - logger.warning_once(
31 - "VLLM_PLE_CPU_OFFLOAD is a legacy setting and may be removed in a "
32 - "future release. Use --engram-config.cpu_offload instead."
33 - )
27 return envs.VLLM_PLE_CPU_OFFLOAD
28
29
30 +def model_has_engram_layers(model_config: "ModelConfig | None") -> bool:
31 + """Whether the model carries n-gram embedding layers."""
32 + if model_config is None:
33 + return False
34 + field = _NGRAM_LAYER_FIELDS.get(model_config.architecture)
35 + if field is None:
36 + return False
37 + return bool(getattr(model_config.hf_text_config, field, None))
38 +
39 +
40 @config
41 class EngramConfig:
42 """Configuration for Engram embedding storage and sharding."""
43
44 cpu_offload: bool = Field(default_factory=_default_cpu_offload)
45 """Store embedding weights in pinned CPU memory for UVA lookup.
43 - Defaults to False, or VLLM_PLE_CPU_OFFLOAD when set for compatibility.
44 - An explicit value takes precedence over the legacy environment variable."""
46 + Defaults to VLLM_PLE_CPU_OFFLOAD, which is enabled by default. An explicit
47 + value takes precedence over the environment variable."""
48
49 embedding_across_dp: bool = False
50 """Shard embeddings across TP and all DP ranks when enabled.
51 Otherwise, each DP rank has a separate TP-sharded embedding replica."""
52
53 + dp_shared_memory: bool = False
54 + """Share CPU-offloaded embedding weights between co-located
55 + DP replicas. Each node stores one copy of every TP shard, reducing host
56 + memory without per-step Engram DP collectives. Requires sufficient
57 + /dev/shm capacity and a shared IPC namespace."""
58 +
59 + @model_validator(mode="after")
60 + def _validate_shared_memory(self) -> Self:
61 + if self.dp_shared_memory and not self.cpu_offload:
62 + raise ValueError("dp_shared_memory requires cpu_offload=True")
63 + return self
64 +
65 def verify_model_config(self, model_config: "ModelConfig | None") -> None:
66 """Reject Engram configuration for models without n-gram embeddings."""
67 from vllm.platforms import current_platform
@@ -64,13 +79,16 @@ class EngramConfig:
79 ):
80 raise ValueError(
81 "EngramConfig requires a model with supported Engram "
67 - "embeddings. Currently only the CUDA Qwen4Exp and DeepSeek "
68 - "V4.1 implementations with non-empty n-gram layer ids are "
69 - "supported."
82 + "embeddings, non-empty n-gram layer ids, and CUDA."
83 )
84
85 def verify_parallel_config(self, parallel_config: "ParallelConfig") -> None:
86 """Reject unsupported embedding parallel topologies."""
87 + if self.dp_shared_memory:
88 + if parallel_config.data_parallel_size <= 1:
89 + raise ValueError("dp_shared_memory requires data_parallel_size > 1.")
90 + if parallel_config.enable_elastic_ep:
91 + raise ValueError("dp_shared_memory is not supported with elastic EP.")
92 if (
93 self.embedding_across_dp
94 and parallel_config.data_parallel_size > 1
@@ -80,6 +98,18 @@ class EngramConfig:
98 "Engram embedding_across_dp is not supported with elastic EP yet."
99 )
100
101 + def verify_load_config(self, load_config: "LoadConfig") -> None:
102 + """Shared tables require a loader that invokes parameter weight callbacks."""
103 + if self.dp_shared_memory and load_config.load_format not in (
104 + "auto",
105 + "safetensors",
106 + "pt",
107 + ):
108 + raise ValueError(
109 + "dp_shared_memory requires load_format 'auto', "
110 + f"'safetensors' or 'pt'; got {load_config.load_format!r}."
111 + )
112 +
113 def get_parallel_size(self, parallel_config: "ParallelConfig") -> int:
114 """Derive the embedding group size from the parallel configuration."""
115 size = parallel_config.tensor_parallel_size
vllm/config/vllm.py
+23 -8
@@ -34,7 +34,7 @@ from .device import DeviceConfig
34 from .diffusion import DiffusionConfig
35 from .ec_manager_config import EncoderCacheManagerConfig
36 from .ec_transfer import ECTransferConfig
37 -from .engram import EngramConfig
37 +from .engram import EngramConfig, model_has_engram_layers
38 from .kernel import KernelConfig
39 from .kv_events import KVEventsConfig
40 from .kv_transfer import KVTransferConfig
@@ -381,7 +381,7 @@ class VllmConfig:
381 attention_config: AttentionConfig = Field(default_factory=AttentionConfig)
382 """Attention configuration."""
383 engram_config: EngramConfig | None = None
384 - """Optional Engram configuration, only valid for supported PLE models."""
384 + """N-gram embedding storage and sharding settings."""
385 mamba_config: MambaConfig = Field(default_factory=MambaConfig)
386 """Mamba configuration."""
387 kernel_config: KernelConfig = Field(default_factory=KernelConfig)
@@ -1218,22 +1218,37 @@ class VllmConfig:
1218 )
1219
1220 def _resolve_and_verify_engram_config(self) -> None:
1221 - """Resolve legacy offload settings and validate model and parallel configs."""
1222 - if self.engram_config is None:
1223 - if not envs.VLLM_PLE_CPU_OFFLOAD:
1224 - return
1225 - self.engram_config = EngramConfig()
1221 + """Resolve defaults and validate n-gram embedding settings."""
1222 + from vllm.platforms import current_platform
1223 +
1224 model_config = self.model_config
1225 speculative_config = self.speculative_config
1226 # Draft configs inherit the target's communication groups and settings.
1229 - # Qwen4Exp MTP itself disables PLE, so validate its target instead.
1227 + # Validate the target because the draft may disable n-gram embeddings.
1228 if (
1229 speculative_config is not None
1230 and model_config is speculative_config.draft_model_config
1231 ):
1232 model_config = speculative_config.target_model_config
1233 + if (
1234 + model_config is not None
1235 + and model_config.architecture == "DeepseekV41ForCausalLM"
1236 + and getattr(model_config.hf_text_config, "engram_layer_ids", None)
1237 + and self.parallel_config.use_ubatching
1238 + ):
1239 + raise ValueError(
1240 + "DeepSeek V4.1 Engram does not support DBO or microbatching. "
1241 + "Disable --enable-dbo and set --ubatch-size to 0."
1242 + )
1243 + if self.engram_config is None:
1244 + if not current_platform.is_cuda() or not model_has_engram_layers(
1245 + model_config
1246 + ):
1247 + return
1248 + self.engram_config = EngramConfig()
1249 self.engram_config.verify_model_config(model_config)
1250 self.engram_config.verify_parallel_config(self.parallel_config)
1251 + self.engram_config.verify_load_config(self.load_config)
1252 logger.info_once("Resolved Engram configuration: %s", str(self.engram_config))
1253
1254 def __post_init__(self):
vllm/distributed/parallel_state.py
+80
@@ -32,6 +32,7 @@ from collections.abc import Callable
32 from contextlib import AbstractContextManager, contextmanager, nullcontext
33 from dataclasses import dataclass
34 from datetime import timedelta
35 +from math import gcd
36 from multiprocessing import shared_memory
37 from typing import TYPE_CHECKING, Any, Protocol
38 from unittest.mock import patch
@@ -1558,6 +1559,23 @@ def get_etp_group() -> GroupCoordinator:
1559 return _ETP
1560
1561
1562 +_ENGRAM_DP: GroupCoordinator | None = None
1563 +
1564 +
1565 +def get_engram_dp_group() -> GroupCoordinator | None:
1566 + """Return the DP replicas that share one engram embedding table.
1567 +
1568 + None when every replica holds a full (TP-sharded) copy, which is the case
1569 + for models without engram layers and for replicas that span nodes.
1570 + """
1571 + return _ENGRAM_DP
1572 +
1573 +
1574 +def get_engram_dp_size() -> int:
1575 + """Number of DP replicas one engram embedding table is sharded over."""
1576 + return _ENGRAM_DP.world_size if _ENGRAM_DP is not None else 1
1577 +
1578 +
1579 _DCP: GroupCoordinator | None = None
1580
1581
@@ -1933,6 +1951,29 @@ def _elastic_join_warmup_ctx() -> AbstractContextManager[Any]:
1951 return nullcontext()
1952
1953
1954 +def _engram_dp_shard_size(
1955 + world_size: int,
1956 + data_parallel_size: int,
1957 + replica_size: int,
1958 +) -> int:
1959 + """Size of DP shards aligned with complete replicas and node boundaries."""
1960 + node_count = get_node_count()
1961 + replicas_per_node, remainder = divmod(world_size, node_count * replica_size)
1962 + if remainder:
1963 + return 1
1964 + # Shards must divide both the DP group and each node's replica count.
1965 + shard_size = gcd(data_parallel_size, replicas_per_node)
1966 + if shard_size == 1 or node_count == 1:
1967 + return shard_size
1968 + ranks_per_node = replicas_per_node * replica_size
1969 + for start in range(0, world_size, ranks_per_node):
1970 + same_node = in_the_same_node_as(get_world_group().cpu_group, start)
1971 + node_ranks = [rank for rank, local in enumerate(same_node) if local]
1972 + if node_ranks != list(range(start, start + ranks_per_node)):
1973 + return 1
1974 + return shard_size
1975 +
1976 +
1977 def initialize_model_parallel(
1978 tensor_model_parallel_size: int = 1,
1979 pipeline_model_parallel_size: int = 1,
@@ -2055,6 +2096,40 @@ def initialize_model_parallel(
2096 group_name="etp",
2097 )
2098
2099 + # Build the node-local DP group used to shard or share an Engram table.
2100 + global _ENGRAM_DP
2101 + assert _ENGRAM_DP is None, "engram data parallel group is already initialized"
2102 + engram_dp_size = 1
2103 + engram_config = config.engram_config
2104 + if (
2105 + engram_config is not None
2106 + and config.model_config is not None
2107 + and config.model_config.architecture == "DeepseekV41ForCausalLM"
2108 + and not enable_elastic_ep
2109 + ):
2110 + engram_dp_size = _engram_dp_shard_size(
2111 + world_size,
2112 + data_parallel_size,
2113 + tensor_model_parallel_size
2114 + * pipeline_model_parallel_size
2115 + * prefill_context_model_parallel_size,
2116 + )
2117 + if engram_dp_size > 1:
2118 + group_ranks = (
2119 + all_ranks.permute(0, 2, 3, 4, 1).reshape(-1, engram_dp_size).unbind(0)
2120 + )
2121 + _ENGRAM_DP = init_model_parallel_group(
2122 + [x.tolist() for x in group_ranks],
2123 + get_world_group().local_rank,
2124 + backend,
2125 + group_name="edp",
2126 + )
2127 + logger.info_once(
2128 + "Engram node-local DP group size: %d (TP=%d)",
2129 + engram_dp_size,
2130 + tensor_model_parallel_size,
2131 + )
2132 +
2133 # Build the DCP model-parallel groups.
2134 global _DCP
2135 assert _DCP is None, "decode context model parallel group is already initialized"
@@ -2304,6 +2379,11 @@ def destroy_model_parallel():
2379 _TP.destroy()
2380 _TP = None
2381
2382 + global _ENGRAM_DP
2383 + if _ENGRAM_DP:
2384 + _ENGRAM_DP.destroy()
2385 + _ENGRAM_DP = None
2386 +
2387 global _DCP
2388 if _DCP:
2389 _DCP.destroy()
vllm/envs.py
+3 -6
@@ -154,7 +154,7 @@ if TYPE_CHECKING:
154 VLLM_ROCM_SHUFFLE_KV_CACHE_LAYOUT: bool = False
155 VLLM_ENABLE_V1_MULTIPROCESSING: bool = True
156 VLLM_LOG_BATCHSIZE_INTERVAL: float = -1
157 - VLLM_PLE_CPU_OFFLOAD: bool = False
157 + VLLM_PLE_CPU_OFFLOAD: bool = True
158 VLLM_DISABLE_COMPILE_CACHE: bool = False
159 VLLM_REPLICATE_EMBED: bool = False
160 VLLM_USE_LAYERNAME: bool = True
@@ -2059,11 +2059,8 @@ environment_variables: dict[str, Callable[[], Any]] = {
2059 "VLLM_LOG_MODEL_INSPECTION": lambda: bool(
2060 int(os.getenv("VLLM_LOG_MODEL_INSPECTION", "0"))
2061 ),
2062 - # Keep Qwen4Exp PLE embedding tables in pinned CPU memory and gather their
2063 - # rows through UVA on a dedicated CUDA stream.
2064 - # Legacy fallback for EngramConfig.cpu_offload, which takes precedence.
2065 - # This environment variable may be removed in a future release.
2066 - "VLLM_PLE_CPU_OFFLOAD": lambda: bool(int(os.getenv("VLLM_PLE_CPU_OFFLOAD", "0"))),
2062 + # Store n-gram embedding tables in pinned CPU memory for UVA lookup.
2063 + "VLLM_PLE_CPU_OFFLOAD": lambda: bool(int(os.getenv("VLLM_PLE_CPU_OFFLOAD", "1"))),
2064 # Debug logging for --enable-mfu-metrics
2065 "VLLM_DEBUG_MFU_METRICS": lambda: bool(
2066 int(os.getenv("VLLM_DEBUG_MFU_METRICS", "0"))
vllm/models/deepseek_v4_1/common/engram.py
+112 -93
@@ -51,8 +51,6 @@ from vllm.model_executor.layers.linear import ReplicatedLinear
51 from vllm.model_executor.layers.quantization import QuantizationConfig
52 from vllm.model_executor.utils import set_weight_attrs
53 from vllm.triton_utils import tl, triton
54 -from vllm.utils.platform_utils import is_uva_available
55 -from vllm.utils.torch_utils import get_accelerator_view_from_cpu_tensor
54
55 logger = init_logger(__name__)
56
@@ -236,6 +234,7 @@ def _write_hash_cache_kernel(
234 tl.store(cache + slot, value, valid)
235
236
237 +# Keep request shapes out of the cache key so one warmed variant covers runtime batches.
238 @triton.jit(
239 do_not_specialize=[
240 "num_tokens",
@@ -462,6 +461,21 @@ class NgramHashState(nn.Module):
461 self._kv_cache_ref = weakref.ref(kv_cache)
462 return True
463
464 + def dummy_hashes(
465 + self, input_ids: torch.Tensor
466 + ) -> tuple[torch.Tensor, torch.Tensor]:
467 + """Participate in DP lookups without valid rows or hash-cache updates."""
468 + num_tokens = input_ids.shape[0]
469 + num_layers, max_ngram = self.multipliers.shape
470 + num_heads = self.primes.shape[-1]
471 + hashes = input_ids.new_full(
472 + (num_tokens, num_layers, (max_ngram - 1) * num_heads),
473 + DEAD_ID,
474 + dtype=torch.int32,
475 + )
476 + keep = torch.zeros(num_tokens, dtype=torch.bool, device=input_ids.device)
477 + return hashes, keep
478 +
479 def forward(
480 self,
481 input_ids: torch.Tensor,
@@ -561,7 +575,16 @@ def _engram_head_shard_weight_loader(
575 param.data.copy_(shard)
576
577
564 -@triton.jit
578 +@triton.jit(
579 + do_not_specialize=[
580 + "vocab_start",
581 + "vocab_end",
582 + "num_rows",
583 + "ids_stride_t",
584 + "ids_stride_h",
585 + "GRID",
586 + ]
587 +)
588 def _engram_lookup_kernel(
589 weight,
590 scales,
@@ -578,7 +601,7 @@ def _engram_lookup_kernel(
601 DIM: tl.constexpr,
602 QUANT_BLOCK: tl.constexpr,
603 BLOCK_R: tl.constexpr,
581 - GRID: tl.constexpr,
604 + GRID,
605 ):
606 """Gather fp8 rows, apply their ue8m0 block scales, write bf16.
607
@@ -620,12 +643,9 @@ def _engram_lookup_kernel(
643
644
645 class ParallelEngramEmbedding(nn.Module):
623 - """The n-gram hash table, sharded by complete hash heads over TP ranks.
624 - Rows stay fp8 and are dequantized with ue8m0 per-32 scales on lookup.
646 + """TP-sharded hash heads with FP8 rows and per-block E8M0 scales."""
647
626 - With `cpu_offload` the shard lives in pinned host memory and is read over
627 - UVA instead of HBM; the TP sharding is unchanged either way.
628 - """
648 + _weight_loader = staticmethod(_engram_head_shard_weight_loader)
649
650 def __init__(
651 self,
@@ -633,89 +653,55 @@ class ParallelEngramEmbedding(nn.Module):
653 dim: int,
654 head_sizes: tuple[int, ...],
655 block_size: int = 32,
636 - cpu_offload: bool = False,
637 - ):
656 + ) -> None:
657 super().__init__()
639 - tp_size = get_tensor_model_parallel_world_size()
640 - tp_rank = get_tensor_model_parallel_rank()
658 assert head_sizes and all(size > 0 for size in head_sizes)
659 assert sum(head_sizes) <= num_embeddings
643 - if cpu_offload and not is_uva_available():
644 - raise RuntimeError("Engram CPU offload requires UVA support")
660 self.num_embeddings = num_embeddings
661 self.dim = dim
662 self.block_size = block_size
663 self.n_hash_cols = len(head_sizes)
649 - self.part_n_hash_cols = triton.cdiv(self.n_hash_cols, tp_size)
650 - self.head_start = tp_rank * self.part_n_hash_cols
664 + self.tp_size = get_tensor_model_parallel_world_size()
665 + num_shards, head_rank = self._get_shard_info()
666 + self.part_n_hash_cols = triton.cdiv(self.n_hash_cols, num_shards)
667 + # TODO: Support row-wise sharding when there are too few hash heads.
668 + assert (num_shards - 1) * self.part_n_hash_cols < self.n_hash_cols, (
669 + f"Engram sharding leaves ranks without hash heads: "
670 + f"{self.n_hash_cols} heads over {num_shards} shards"
671 + )
672 + self.head_start = head_rank * self.part_n_hash_cols
673 head_end = self.head_start + self.part_n_hash_cols
674 self.vocab_start_idx = sum(head_sizes[: self.head_start])
675 self.vocab_end_idx = sum(head_sizes[:head_end])
676 self.part_num_embeddings = self.vocab_end_idx - self.vocab_start_idx
655 - self.tp_size = tp_size
656 - self.cpu_offload = cpu_offload
657 - self._views: tuple[torch.Tensor, torch.Tensor] | None = None
658 - self._view_src: tuple[int, int] | None = None
677 self._num_sms = torch.cuda.get_device_properties(
678 torch.accelerator.current_device_index()
679 ).multi_processor_count
662 -
663 - # Explicit device: model init runs under a `torch.device("cuda")`
664 - # context, which would otherwise put the shard in HBM.
665 - kwargs = {"device": "cpu", "pin_memory": True} if cpu_offload else {}
666 - self.weight = nn.Parameter(
667 - torch.empty(
668 - self.part_num_embeddings, dim, dtype=torch.float8_e4m3fn, **kwargs
669 - ),
670 - requires_grad=False,
671 - )
672 - self.weight_scale_inv = nn.Parameter(
673 - torch.empty(
674 - self.part_num_embeddings,
675 - dim // block_size,
676 - dtype=torch.uint8,
677 - **kwargs,
678 - ),
679 - requires_grad=False,
680 - )
680 + weight, scales = self._allocate_weights()
681 + self.weight = nn.Parameter(weight, requires_grad=False)
682 + self.weight_scale_inv = nn.Parameter(scales, requires_grad=False)
683 for param in (self.weight, self.weight_scale_inv):
684 set_weight_attrs(
685 param,
686 {
685 - "weight_loader": _engram_head_shard_weight_loader,
687 + "weight_loader": self._weight_loader,
688 "engram_vocab_start": self.vocab_start_idx,
689 },
690 )
689 - if cpu_offload:
690 - # Constant dummy values avoid randomizing huge CPU lookup tables.
691 - set_weight_attrs(self.weight, {"dummy_weight_value": 1.0})
692 - # The ue8m0 encoding of scale 1.0 is exponent byte 127.
693 - set_weight_attrs(self.weight_scale_inv, {"dummy_weight_value": 127})
694 - logger.info(
695 - "Engram table offloaded to pinned host memory: %d rows x %d, "
696 - "%.2f GiB per rank",
697 - self.part_num_embeddings,
698 - dim,
699 - self.part_num_embeddings * (dim + dim // block_size) / 1024**3,
700 - )
691
702 - def _storage(self) -> tuple[torch.Tensor, torch.Tensor]:
703 - """Parameters when resident, else cached UVA views of the pinned shard.
692 + def _get_shard_info(self) -> tuple[int, int]:
693 + return self.tp_size, get_tensor_model_parallel_rank()
694
705 - Rebuilt if anything swaps `.data`, so a stale device pointer cannot
706 - survive silently.
707 - """
708 - if not self.cpu_offload:
709 - return self.weight.data, self.weight_scale_inv.data
710 - src = (self.weight.data_ptr(), self.weight_scale_inv.data_ptr())
711 - if self._view_src != src:
712 - self._views = (
713 - get_accelerator_view_from_cpu_tensor(self.weight.data),
714 - get_accelerator_view_from_cpu_tensor(self.weight_scale_inv.data),
715 - )
716 - self._view_src = src
717 - assert self._views is not None
718 - return self._views
695 + def _allocate_weights(self) -> tuple[torch.Tensor, torch.Tensor]:
696 + return (
697 + torch.empty(self.part_num_embeddings, self.dim, dtype=torch.float8_e4m3fn),
698 + torch.empty(
699 + self.part_num_embeddings, self.dim // self.block_size, dtype=torch.uint8
700 + ),
701 + )
702 +
703 + def _storage(self) -> tuple[torch.Tensor, torch.Tensor]:
704 + return self.weight.data, self.weight_scale_inv.data
705
706 def lookup(
707 self, indices: torch.Tensor, out: torch.Tensor, background: bool = False
@@ -753,7 +739,7 @@ class ParallelEngramEmbedding(nn.Module):
739
740 def forward(self, indices: torch.Tensor) -> torch.Tensor:
741 """indices: [num_tokens, n_hash_cols] -> [num_tokens, n_hash_cols, dim]
756 - bf16, gathered from all TP shards."""
742 + bf16, gathered from all shards for this replica's tokens."""
743 out = torch.empty(
744 (indices.shape[0], self.part_n_hash_cols, self.dim),
745 dtype=torch.bfloat16,
@@ -762,8 +748,7 @@ class ParallelEngramEmbedding(nn.Module):
748 self.lookup(indices, out)
749 if self.tp_size > 1:
750 out = tensor_model_parallel_all_gather(out, dim=1)
765 - out = out[:, : self.n_hash_cols]
766 - return out
751 + return out[:, : self.n_hash_cols]
752
753
754 @triton.jit(do_not_specialize=["num_kv_tokens"])
@@ -859,7 +844,7 @@ def _fused_engram_post_wkv_kernel(
844
845
846 @triton.jit(do_not_specialize=["num_tokens", "token_start", "num_elements"])
862 -def _engram_sp_rows_kernel(
847 +def _engram_select_rows_kernel(
848 gathered,
849 output,
850 num_tokens,
@@ -880,6 +865,32 @@ def _engram_sp_rows_kernel(
865 tl.store(output + offsets, values, offsets < num_elements)
866
867
868 +def _engram_select_rows(
869 + gathered: torch.Tensor,
870 + output: torch.Tensor,
871 + source_tokens: int,
872 + token_start: int,
873 + local_width: int,
874 +) -> None:
875 + """Copy one token window out of a rank-major gathered buffer.
876 +
877 + Both gathers land rank-major ([rank][token][local width]); this walks the
878 + window the rank keeps and lays its ranks out side by side as width.
879 + """
880 + if output.numel() == 0:
881 + return
882 + _engram_select_rows_kernel[(triton.cdiv(output.numel(), 1024),)](
883 + gathered,
884 + output,
885 + source_tokens,
886 + token_start,
887 + output.numel(),
888 + local_width,
889 + output.shape[1] * output.shape[2],
890 + BLOCK_SIZE=1024,
891 + )
892 +
893 +
894 class Engram(nn.Module):
895 """Writes an n-gram lookup into the residual stream, gated by how well it
896 matches that stream.
@@ -910,13 +921,7 @@ class Engram(nn.Module):
921 # Named ``embed_tokens`` so the checkpoint's ``engram.embed.weight``
922 # survives the mapper's ``embed.weight`` -> ``embed_tokens.weight``
923 # suffix rule.
913 - engram_config = get_current_vllm_config().engram_config
914 - self.embed_tokens = ParallelEngramEmbedding(
915 - layout.num_embeddings[layer_hash_index],
916 - layout.head_dim,
917 - tuple(size for order in layout.primes[layer_hash_index] for size in order),
918 - cpu_offload=engram_config.cpu_offload if engram_config else True,
919 - )
924 + self.embed_tokens = self._create_embedding(layout, layer_hash_index)
925 n_hash_cols = (layout.max_ngram_size - 1) * layout.n_heads
926 self.wkv = ReplicatedLinear(
927 n_hash_cols * layout.head_dim,
@@ -936,40 +941,54 @@ class Engram(nn.Module):
941 )
942
943 max_tokens = get_current_vllm_config().scheduler_config.max_num_batched_tokens
939 - # Keep lookup results alive across breakable graph segments.
944 + self._init_staging(max_tokens, layout.head_dim)
945 +
946 + def _create_embedding(
947 + self, layout: EngramLayout, layer_hash_index: int
948 + ) -> ParallelEngramEmbedding:
949 + return ParallelEngramEmbedding(
950 + layout.num_embeddings[layer_hash_index],
951 + layout.head_dim,
952 + tuple(size for order in layout.primes[layer_hash_index] for size in order),
953 + )
954 +
955 + def _init_staging(self, max_tokens: int, head_dim: int) -> None:
956 + # Persistent storage keeps lookup addresses stable across graph replays.
957 self.staged_rows = torch.empty(
958 max_tokens,
959 self.embed_tokens.part_n_hash_cols,
943 - layout.head_dim,
960 + head_dim,
961 dtype=torch.bfloat16,
962 )
963
964 def prepare_embeddings(self, hash_ids: torch.Tensor) -> None:
948 - """Gather this layer's rows on the main stream before decoder layers."""
949 - self.embed_tokens.lookup(hash_ids, self.staged_rows[: hash_ids.shape[0]])
965 + """Look up rows before the decoder layers consume them."""
966 + rows = self.staged_rows[: hash_ids.shape[0]]
967 + assert rows.shape[0] == hash_ids.shape[0], "engram staging buffer too small"
968 + self.embed_tokens.lookup(hash_ids, rows)
969 +
970 + def _ready_rows(self, num_tokens: int) -> torch.Tensor:
971 + return self.staged_rows[:num_tokens]
972
973 def embed(self, hash_ids: torch.Tensor) -> torch.Tensor:
974 """Gather heads, returning only local tokens when SP is enabled."""
953 - rows = self.staged_rows[: hash_ids.shape[0]]
975 + rows = self._ready_rows(hash_ids.shape[0])
976 if self.embed_tokens.tp_size == 1:
955 - return rows
977 + return rows[:, : self.embed_tokens.n_hash_cols]
978 if self.use_sequence_parallel:
979 tp_size = self.embed_tokens.tp_size
980 num_tokens, local_heads, dim = rows.shape
981 gathered = tensor_model_parallel_all_gather(rows, dim=0)
982 chunk = (num_tokens + tp_size - 1) // tp_size
961 - rows = rows.new_empty((chunk, self.embed_tokens.n_hash_cols, dim))
962 - _engram_sp_rows_kernel[(triton.cdiv(rows.numel(), 1024),)](
983 + out = rows.new_empty((chunk, self.embed_tokens.n_hash_cols, dim))
984 + _engram_select_rows(
985 gathered,
964 - rows,
986 + out,
987 num_tokens,
988 get_tensor_model_parallel_rank() * chunk,
967 - rows.numel(),
989 local_heads * dim,
969 - self.embed_tokens.n_hash_cols * dim,
970 - BLOCK_SIZE=1024,
990 )
972 - return rows
991 + return out
992 rows = tensor_model_parallel_all_gather(rows, dim=1)
993 return rows[:, : self.embed_tokens.n_hash_cols]
994
vllm/models/deepseek_v4_1/nvidia/engram.py new
+403
@@ -0,0 +1,403 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +"""NVIDIA Engram DP sharding, shared host storage, and asynchronous prefetch."""
4 +
5 +import mmap
6 +import tempfile
7 +import weakref
8 +from contextlib import ExitStack
9 +
10 +import numpy as np
11 +import torch
12 +
13 +from vllm.compilation.breakable_cudagraph import eager_break_during_capture
14 +from vllm.config import get_current_vllm_config
15 +from vllm.distributed import (
16 + get_dp_group,
17 + get_engram_dp_group,
18 + get_engram_dp_size,
19 + get_tensor_model_parallel_rank,
20 + tensor_model_parallel_all_gather,
21 +)
22 +from vllm.distributed.parallel_state import GroupCoordinator
23 +from vllm.forward_context import get_forward_context
24 +from vllm.logger import init_logger
25 +from vllm.model_executor.utils import set_weight_attrs
26 +from vllm.models.deepseek_v4_1.common.engram import (
27 + DEAD_ID,
28 + EngramLayout,
29 + _engram_head_shard_weight_loader,
30 + _engram_select_rows,
31 +)
32 +from vllm.models.deepseek_v4_1.common.engram import (
33 + Engram as BaseEngram,
34 +)
35 +from vllm.models.deepseek_v4_1.common.engram import (
36 + ParallelEngramEmbedding as BaseParallelEngramEmbedding,
37 +)
38 +from vllm.utils.platform_utils import is_uva_available
39 +from vllm.utils.torch_utils import get_accelerator_view_from_cpu_tensor
40 +
41 +logger = init_logger(__name__)
42 +
43 +
44 +def engram_head_shard_rank() -> int:
45 + """This rank's slot among the hash-head shards of one engram table.
46 +
47 + TP-major, so the shards a DP gather brings in are contiguous heads and
48 + the following TP gather completes the head order.
49 + """
50 + dp_group = get_engram_dp_group()
51 + dp_size = dp_group.world_size if dp_group is not None else 1
52 + dp_rank = dp_group.rank_in_group if dp_group is not None else 0
53 + return get_tensor_model_parallel_rank() * dp_size + dp_rank
54 +
55 +
56 +def engram_gathered_num_tokens() -> int:
57 + """Per-replica token slot for the node-local Engram DP group."""
58 + dp_metadata = get_forward_context().dp_metadata
59 + if dp_metadata is None:
60 + raise RuntimeError("a DP-shared engram table needs DP token metadata")
61 + group = get_engram_dp_group()
62 + assert group is not None
63 + # Engram groups are contiguous slices of the full DP group.
64 + start = get_dp_group().rank_in_group - group.rank_in_group
65 + return int(
66 + dp_metadata.num_tokens_across_dp_cpu[start : start + group.world_size].max()
67 + )
68 +
69 +
70 +def gather_engram_hashes(
71 + hash_ids: torch.Tensor, *, dp_shared_memory: bool = False
72 +) -> torch.Tensor:
73 + """Collect the n-gram ids of every DP replica sharing one table.
74 +
75 + Replicas are padded to a common token slot, so the gathered shape is
76 + static under CUDA graph capture (where DP already pads alike).
77 + """
78 + dp_group = get_engram_dp_group()
79 + if dp_group is None or dp_shared_memory:
80 + return hash_ids
81 + slot = engram_gathered_num_tokens()
82 + if hash_ids.shape[0] > slot:
83 + raise ValueError("Engram token count exceeds the DP token slot")
84 + if hash_ids.shape[0] < slot:
85 + pad = hash_ids.new_full(
86 + (slot - hash_ids.shape[0], *hash_ids.shape[1:]), DEAD_ID
87 + )
88 + hash_ids = torch.cat((hash_ids, pad))
89 + return dp_group.all_gather(hash_ids, dim=0)
90 +
91 +
92 +class DPSharedEngramStorage:
93 + """Registered host weights shared by a node-local DP group with one writer."""
94 +
95 + def __init__(
96 + self, num_rows: int, dim: int, block_size: int, group: GroupCoordinator
97 + ) -> None:
98 + self.group = group
99 + weight_bytes = num_rows * dim
100 + storage = self._allocate(weight_bytes + weight_bytes // block_size)
101 + self.weight = storage[:weight_bytes].view(torch.float8_e4m3fn).view(-1, dim)
102 + self.weight_scale_inv = storage[weight_bytes:].view(-1, dim // block_size)
103 + self._views: tuple[torch.Tensor, torch.Tensor] | None = None
104 +
105 + def _allocate(self, num_bytes: int) -> torch.Tensor:
106 + """Map and register one physical allocation across a node-local DP group."""
107 + from vllm.distributed.device_communicators.shm_broadcast import (
108 + check_shm_free_space,
109 + )
110 +
111 + group = self.group
112 + with ExitStack() as stack:
113 + path, error = None, None
114 + if group.rank_in_group == 0:
115 + try:
116 + check_shm_free_space(num_bytes)
117 + backing_file = stack.enter_context(
118 + tempfile.NamedTemporaryFile(
119 + prefix="vllm_engram_", dir="/dev/shm"
120 + )
121 + )
122 + backing_file.truncate(num_bytes)
123 + path = backing_file.name
124 + except Exception as exc:
125 + error = f"{type(exc).__name__}: {exc}"
126 + path, error = group.broadcast_object((path, error))
127 + if error is not None:
128 + raise RuntimeError(
129 + "Engram shared-memory creation failed on EDP rank 0: " + error
130 + )
131 +
132 + mapping = owner = tensor = finalizer = None
133 + stage = "open"
134 + try:
135 + try:
136 + with open(path, "r+b") as file:
137 + stage = "mmap"
138 + mapping = mmap.mmap(
139 + file.fileno(), num_bytes, flags=mmap.MAP_SHARED
140 + )
141 + stage = "cudaHostRegister"
142 + owner = np.frombuffer(mapping, dtype=np.uint8)
143 + pointer = owner.ctypes.data
144 + tensor = torch.from_numpy(owner)
145 + result = torch.cuda.cudart().cudaHostRegister(pointer, num_bytes, 0)
146 + if result.value != 0:
147 + raise RuntimeError(f"cudaHostRegister failed: {result}")
148 + finalizer = weakref.finalize(
149 + owner, self._unregister, mapping, pointer
150 + )
151 + finalizer.atexit = False # type: ignore[misc]
152 + # The UVA helper otherwise allocates a private pinned copy.
153 + if not tensor.is_pinned():
154 + raise RuntimeError(
155 + "CUDA did not recognize the shared Engram registration"
156 + )
157 + except Exception as exc:
158 + error = f"{stage}: {type(exc).__name__}: {exc}"
159 +
160 + errors: list[str | None] = [None] * group.world_size
161 + # Also fences peer mappings before the leader unlinks the file.
162 + torch.distributed.all_gather_object(
163 + errors, error, group=group.cpu_group
164 + )
165 + failures = "; ".join(
166 + f"EDP rank {rank}: {error}"
167 + for rank, error in enumerate(errors)
168 + if error is not None
169 + )
170 + if failures:
171 + raise RuntimeError(
172 + "Engram shared-memory initialization failed: " + failures
173 + )
174 + assert tensor is not None
175 + return tensor
176 + except Exception:
177 + if finalizer is not None:
178 + finalizer()
179 + tensor = owner = None
180 + if mapping is not None:
181 + mapping.close()
182 + raise
183 +
184 + @staticmethod
185 + def _unregister(mapping: mmap.mmap, pointer: int) -> None:
186 + # Torch storage retains the numpy owner, including through cached UVA views.
187 + # Keep its mmap alive until CUDA has released the registration.
188 + result = torch.cuda.cudart().cudaHostUnregister(pointer)
189 + if result.value != 0:
190 + logger.warning("Engram cudaHostUnregister failed: %s", result)
191 +
192 + def load_weight(
193 + self, param: torch.nn.Parameter, loaded_weight: torch.Tensor
194 + ) -> None:
195 + if self.group.rank_in_group == 0:
196 + _engram_head_shard_weight_loader(param, loaded_weight)
197 + # Read order may differ across ranks. Equal load counts ensure all shared
198 + # weights are ready after the last weight-loader call returns.
199 + torch.distributed.barrier(group=self.group.cpu_group)
200 +
201 + def get_views(
202 + self, weight: torch.Tensor, scales: torch.Tensor
203 + ) -> tuple[torch.Tensor, torch.Tensor]:
204 + if (weight.data_ptr(), scales.data_ptr()) != (
205 + self.weight.data_ptr(),
206 + self.weight_scale_inv.data_ptr(),
207 + ):
208 + raise RuntimeError("Shared Engram parameter storage must not be replaced")
209 + if self._views is None:
210 + self._views = (
211 + get_accelerator_view_from_cpu_tensor(self.weight),
212 + get_accelerator_view_from_cpu_tensor(self.weight_scale_inv),
213 + )
214 + return self._views
215 +
216 +
217 +class ParallelEngramEmbedding(BaseParallelEngramEmbedding):
218 + """Extend TP lookup with DP head sharding or shared, CPU-offloaded TP slices."""
219 +
220 + _shared_memory: DPSharedEngramStorage | None = None
221 +
222 + def __init__(
223 + self,
224 + num_embeddings: int,
225 + dim: int,
226 + head_sizes: tuple[int, ...],
227 + block_size: int = 32,
228 + cpu_offload: bool = False,
229 + dp_shared_memory: bool = False,
230 + ) -> None:
231 + self.cpu_offload = cpu_offload
232 + self.dp_shared_memory = dp_shared_memory
233 + self.dp_size = get_engram_dp_size()
234 + if dp_shared_memory:
235 + if not cpu_offload:
236 + raise ValueError("dp_shared_memory requires cpu_offload=True")
237 + if self.dp_size <= 1:
238 + raise ValueError(
239 + "dp_shared_memory requires a node-local Engram DP "
240 + f"group with size > 1; effective Engram DP size is {self.dp_size}. "
241 + "Check that the node layout and rank placement allow complete "
242 + "DP replicas to be co-located."
243 + )
244 + self.dp_size = 1
245 + if cpu_offload and not is_uva_available():
246 + raise RuntimeError("Engram CPU offload requires UVA support")
247 + self._views: tuple[torch.Tensor, torch.Tensor] | None = None
248 + self._view_src: tuple[int, int] | None = None
249 + super().__init__(num_embeddings, dim, head_sizes, block_size)
250 + if cpu_offload:
251 + # Constant dummy values avoid randomizing huge CPU lookup tables.
252 + set_weight_attrs(self.weight, {"dummy_weight_value": 1.0})
253 + # The ue8m0 encoding of scale 1.0 is exponent byte 127.
254 + set_weight_attrs(self.weight_scale_inv, {"dummy_weight_value": 127})
255 + logger.info(
256 + "Engram table offloaded to pinned host memory: %d rows x %d, "
257 + "%.2f GiB %s",
258 + self.part_num_embeddings,
259 + dim,
260 + self.part_num_embeddings * (dim + dim // block_size) / 1024**3,
261 + "shared across DP replicas" if dp_shared_memory else "per rank",
262 + )
263 +
264 + def _get_shard_info(self) -> tuple[int, int]:
265 + if self.dp_size == 1:
266 + return super()._get_shard_info()
267 + return self.tp_size * self.dp_size, engram_head_shard_rank()
268 +
269 + def _allocate_weights(self) -> tuple[torch.Tensor, torch.Tensor]:
270 + if self.dp_shared_memory:
271 + group = get_engram_dp_group()
272 + assert group is not None
273 + storage = DPSharedEngramStorage(
274 + self.part_num_embeddings, self.dim, self.block_size, group
275 + )
276 + self._shared_memory = storage
277 + self._weight_loader = storage.load_weight
278 + return storage.weight, storage.weight_scale_inv
279 + if not self.cpu_offload:
280 + return super()._allocate_weights()
281 + # Model initialization may be inside a CUDA device context.
282 + return (
283 + torch.empty(
284 + self.part_num_embeddings,
285 + self.dim,
286 + dtype=torch.float8_e4m3fn,
287 + device="cpu",
288 + pin_memory=True,
289 + ),
290 + torch.empty(
291 + self.part_num_embeddings,
292 + self.dim // self.block_size,
293 + dtype=torch.uint8,
294 + device="cpu",
295 + pin_memory=True,
296 + ),
297 + )
298 +
299 + def _storage(self) -> tuple[torch.Tensor, torch.Tensor]:
300 + if self._shared_memory is not None:
301 + return self._shared_memory.get_views(self.weight, self.weight_scale_inv)
302 + if not self.cpu_offload:
303 + return super()._storage()
304 + src = (self.weight.data_ptr(), self.weight_scale_inv.data_ptr())
305 + if self._view_src != src:
306 + self._views = (
307 + get_accelerator_view_from_cpu_tensor(self.weight.data),
308 + get_accelerator_view_from_cpu_tensor(self.weight_scale_inv.data),
309 + )
310 + self._view_src = src
311 + assert self._views is not None
312 + return self._views
313 +
314 + def forward(self, indices: torch.Tensor) -> torch.Tensor:
315 + if self.dp_size == 1:
316 + return super().forward(indices)
317 + num_tokens = indices.shape[0]
318 + indices = gather_engram_hashes(indices)
319 + out = torch.empty(
320 + (indices.shape[0], self.part_n_hash_cols, self.dim),
321 + dtype=torch.bfloat16,
322 + device=indices.device,
323 + )
324 + self.lookup(indices, out)
325 + out = _gather_engram_rows(out, num_tokens)
326 + if self.tp_size > 1:
327 + out = tensor_model_parallel_all_gather(out, dim=1)
328 + return out[:, : self.n_hash_cols]
329 +
330 +
331 +def _gather_engram_rows(staged: torch.Tensor, num_tokens: int) -> torch.Tensor:
332 + """Exchange DP tokens for heads, retaining only this replica's tokens."""
333 + dp_group = get_engram_dp_group()
334 + assert dp_group is not None
335 + slot, remainder = divmod(staged.shape[0], dp_group.world_size)
336 + assert remainder == 0 and 0 <= num_tokens <= slot
337 + gathered = dp_group.all_gather(staged, dim=0)
338 + local_heads, dim = staged.shape[1:]
339 + rows = staged.new_empty((num_tokens, dp_group.world_size * local_heads, dim))
340 + _engram_select_rows(
341 + gathered,
342 + rows,
343 + staged.shape[0],
344 + dp_group.rank_in_group * slot,
345 + local_heads * dim,
346 + )
347 + return rows
348 +
349 +
350 +class Engram(BaseEngram):
351 + """NVIDIA Engram with asynchronous offload and node-local DP lookup."""
352 +
353 + _prefetch_stream: torch.cuda.Stream | None = None
354 +
355 + def _create_embedding(
356 + self, layout: EngramLayout, layer_hash_index: int
357 + ) -> ParallelEngramEmbedding:
358 + engram_config = get_current_vllm_config().engram_config
359 + assert engram_config is not None
360 + return ParallelEngramEmbedding(
361 + layout.num_embeddings[layer_hash_index],
362 + layout.head_dim,
363 + tuple(size for order in layout.primes[layer_hash_index] for size in order),
364 + cpu_offload=engram_config.cpu_offload,
365 + dp_shared_memory=engram_config.dp_shared_memory,
366 + )
367 +
368 + def _init_staging(self, max_tokens: int, head_dim: int) -> None:
369 + super()._init_staging(max_tokens * self.embed_tokens.dp_size, head_dim)
370 + if self.embed_tokens.cpu_offload:
371 + self._prefetch_stream = torch.cuda.Stream(device=self.staged_rows.device)
372 +
373 + def prepare_embeddings(self, hash_ids: torch.Tensor) -> None:
374 + """Prefetch local shared rows or the DP group's gathered hash IDs."""
375 + if self._prefetch_stream is None:
376 + return super().prepare_embeddings(hash_ids)
377 + rows = self.staged_rows[: hash_ids.shape[0]]
378 + assert rows.shape[0] == hash_ids.shape[0], "engram staging buffer too small"
379 + self._start_prefetch(hash_ids, rows, self._prefetch_stream)
380 +
381 + @eager_break_during_capture
382 + def _start_prefetch(
383 + self, hash_ids: torch.Tensor, rows: torch.Tensor, stream: torch.cuda.Stream
384 + ) -> None:
385 + # Eager boundaries let the lookup span piecewise graph segments.
386 + stream.wait_stream(torch.cuda.current_stream())
387 + # Keep temporary hash storage alive until lookup finishes reading it.
388 + hash_ids.record_stream(stream)
389 + with torch.cuda.stream(stream):
390 + self.embed_tokens.lookup(hash_ids, rows, background=True)
391 +
392 + @eager_break_during_capture
393 + def _finish_prefetch(self, stream: torch.cuda.Stream) -> None:
394 + torch.cuda.current_stream().wait_stream(stream)
395 +
396 + def _ready_rows(self, num_tokens: int) -> torch.Tensor:
397 + if self._prefetch_stream is not None:
398 + self._finish_prefetch(self._prefetch_stream)
399 + if self.embed_tokens.dp_size > 1:
400 + slot = engram_gathered_num_tokens()
401 + staged = self.staged_rows[: slot * self.embed_tokens.dp_size]
402 + return _gather_engram_rows(staged, num_tokens)
403 + return super()._ready_rows(num_tokens)
vllm/models/deepseek_v4_1/nvidia/model.py
+16 -2
@@ -12,6 +12,7 @@ import vllm.envs as envs
12 from vllm.config import VllmConfig
13 from vllm.config.kernel import MEGA_MOE_BACKENDS
14 from vllm.distributed import (
15 + get_engram_dp_size,
16 get_pp_group,
17 get_tensor_model_parallel_rank,
18 get_tensor_model_parallel_world_size,
@@ -74,8 +75,9 @@ from vllm.utils.math_utils import cdiv
75 from vllm.v1.attention.backends.registry import AttentionBackendEnum
76 from vllm.v1.worker.ubatching import dbo_current_ubatch_id
77
77 -from ..common.engram import Engram, EngramLayout, NgramHashState
78 +from ..common.engram import EngramLayout, NgramHashState
79 from ..common.mm_preprocess import IMAGE_SENTINEL_BASE_ID, image_sentinel_mask
80 +from .engram import Engram, gather_engram_hashes
81
82 if typing.TYPE_CHECKING:
83 from vllm.v1.attention.backends.mla.sparse_swa import DeepseekSparseSWAMetadata
@@ -466,6 +468,9 @@ class DeepseekV4Model(nn.Module, EagleModelMixin):
468 # layer's sliding-window KV cache. Only PP ranks owning an engram
469 # layer need it.
470 self.engram_hash: NgramHashState | None = None
471 + self.engram_dp_shared_memory = bool(
472 + vllm_config.engram_config and vllm_config.engram_config.dp_shared_memory
473 + )
474 self.engram_swa_prefix: str | None = None
475 if self.engram_layout is not None:
476 local_engram = any(
@@ -594,12 +599,21 @@ class DeepseekV4Model(nn.Module, EagleModelMixin):
599 swa_metadata.slot_mapping,
600 swa_metadata.block_table,
601 )
602 + elif not self.engram_dp_shared_memory and get_engram_dp_size() > 1:
603 + # DP-sharded lookups are collective, so a replica skipping the
604 + # hash still has to reach them.
605 + engram_hashes, engram_mask = self.engram_hash.dummy_hashes(input_ids)
606 + if engram_hashes is not None:
607 # Gather all Engram rows before entering the decoder layers.
608 + # One gather feeds every layer sharing the DP-split table.
609 + gathered_hashes = gather_engram_hashes(
610 + engram_hashes, dp_shared_memory=self.engram_dp_shared_memory
611 + )
612 for layer in islice(self.layers, self.start_layer, self.end_layer):
613 engram = getattr(layer, "engram", None)
614 if engram is not None:
615 engram.prepare_embeddings(
602 - engram_hashes[:, engram.layer_hash_index]
616 + gathered_hashes[:, engram.layer_hash_index]
617 )
618
619 full_num_tokens = positions.shape[0]