main
py 1,753 lines 73.1 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Consumer-side unit tests for the sharded-RDT backend.
4
5 The consumer engine's planning core is pure — it turns the baked plan plus the
6 driver's gather-group partition into a static `_CallPlan` with no pulls, no
7 model and no Ray — so most of it is exercised here on CPU/meta tensors only.
8 These tests pin the values the engine produces today (chunk boundaries, packed
9 byte offsets, recorded op chains) so a refactor that changes them fails loudly
10 instead of silently shipping different bytes.
11
12 `TestBakeOnARealModel` at the end is the exception: it needs a GPU, because the
13 bake only exists to be driven through a real `model.load_weights`, and the parts
14 that matter there — that the monkeypatched loaders are put back, and that the
15 recorded op chains reproduce what the loaders did — cannot be faked.
16
17 `tests/distributed/test_sharded_rdt_trainer.py` covers the trainer (producer)
18 side; `test_sharded_rdt_producer.py` covers its serve-actor protocol.
19 """
20
21 from types import SimpleNamespace
22
23 import pytest
24 import torch
25
26 from vllm.distributed.weight_transfer.base import layerwise_groups
27 from vllm.distributed.weight_transfer.sharded_rdt_common import (
28 ALLOWED_OPS,
29 SUPPORTED_OPS,
30 RdtRouter,
31 assign_producer_indices,
32 buffer_alloc_bytes,
33 )
34 from vllm.distributed.weight_transfer.sharded_rdt_engine import (
35 ShardedRDTWeightTransferEngine,
36 ShardedRDTWeightTransferInitInfo,
37 _dtype_from_name,
38 )
39 from vllm.distributed.weight_transfer.sharded_rdt_fake import (
40 BakeSink,
41 FakeRDTTensor,
42 _Scatter,
43 _UnsupportedFakeOp,
44 )
45
46 META = torch.device("meta")
47
48
49 # ---------------------------------------------------------------------------
50 # Harness
51 # ---------------------------------------------------------------------------
52
53
54 class _FakeLayer:
55 """Stand-in for a leaf module. The planner only stores it and keys on
56 ``id()``; nothing in the plan path touches its attributes."""
57
58 def __init__(self, tag: str) -> None:
59 self.tag = tag
60
61 def __repr__(self) -> str:
62 return f"_FakeLayer({self.tag!r})"
63
64
65 def _module(layer, copies):
66 """What the bake records for one leaf module: a scatter list whose entries
67 all carry that module as their ``layer``."""
68 for c in copies:
69 c.layer = layer
70 return copies
71
72
73 def _copy(
74 name,
75 layer_param="weight",
76 *,
77 offset=0,
78 shape=(4,),
79 ops=(),
80 layer=None,
81 dtype=torch.bfloat16,
82 ):
83 """A `_Scatter` as the bake would record it: source key, owning layer, and
84 the meta destination region."""
85 stride = (
86 (1,)
87 if len(shape) == 1
88 else tuple(
89 int(torch.empty(shape, device=META).stride()[i]) for i in range(len(shape))
90 )
91 )
92 return _Scatter(
93 layer=layer,
94 param_name=layer_param,
95 src=(name, tuple(ops)),
96 offset=offset,
97 shape=tuple(shape),
98 stride=stride,
99 dtype=dtype,
100 )
101
102
103 def _planner(baked, *, name_meta, live=None, held_by=None, group_lens=None):
104 """A planning-only engine.
105
106 `__init__` needs a config/model/device the planner never reads, so build the
107 instance without it and set exactly the state `_build_call_plan` consumes:
108 the baked plan, the name metadata, the live-name set, and a bound router.
109
110 ``held_by``: name -> the ranks holding it. Names left out are held by every
111 producer (the replicated class 0). Default: one producer holding everything,
112 the degenerate router. Stand-in ranks 0-3 let plan tests assert chunk
113 structure; owner VALUES are pinned by TestCallPlanRouting.
114 """
115 names = list(name_meta)
116 if held_by:
117 n_prod = max(r for owners in held_by.values() for r in owners) + 1
118 owner_sets = [list(range(n_prod))]
119 seen = {tuple(range(n_prod)): 0}
120 classes = []
121 for n in names:
122 key = tuple(sorted(held_by.get(n, range(n_prod))))
123 if key not in seen:
124 seen[key] = len(owner_sets)
125 owner_sets.append(list(key))
126 classes.append(seen[key])
127 else:
128 n_prod, owner_sets, classes = 1, [[0]], [0] * len(names)
129 eng = object.__new__(ShardedRDTWeightTransferEngine)
130 eng._name_to_plan = dict(baked)
131 eng._name_meta = dict(name_meta)
132 eng._live_names = set(live or name_meta)
133 router = RdtRouter(
134 n_prod, 1, owner_sets, classes, names, group_lens or [1] * len(names)
135 )
136 router.bind([object()] * n_prod, [object()] * n_prod, 0)
137 eng._router = router
138 return eng
139
140
141 def _one_module_per_layer(n_layers, *, dtype="bfloat16", numel=4):
142 """`(baked, name_meta, names, group_lens)` for a pre / N-layer / post model
143 where every gather group is one fully-baked leaf module."""
144 names = ["embed.weight"]
145 names += [f"model.layers.{i}.w" for i in range(n_layers)]
146 names += ["norm.weight"]
147 baked = {}
148 for n in names:
149 layer = _FakeLayer(n)
150 baked[n] = _module(layer, [_copy(n, shape=(numel,))])
151 name_meta = {n: (dtype, [numel]) for n in names}
152 group_lens = [1] * len(names)
153 return baked, name_meta, names, group_lens
154
155
156 # ---------------------------------------------------------------------------
157 # FakeRDTTensor: op-chain recording
158 # ---------------------------------------------------------------------------
159
160
161 class TestFakeOpChains:
162 """Every allowlisted op must append itself to the chain and hand back a
163 child whose shape/dtype PyTorch itself computed. The chain is the wire
164 format the producer replays, so its exact contents are load-bearing."""
165
166 def _fake(self, shape=(4, 6), dtype=torch.bfloat16, sink=None):
167 return FakeRDTTensor(
168 name="w", shape=torch.Size(shape), dtype=dtype, device=META, sink=sink
169 )
170
171 def test_bare_fake_has_metadata_but_no_chain(self):
172 t = self._fake()
173 assert t.shape == (4, 6)
174 assert t.dtype is torch.bfloat16
175 assert t._key() == ("w", ())
176
177 @pytest.mark.parametrize(
178 "call,expect_op,expect_shape",
179 [
180 (lambda t: t.narrow(0, 1, 2), ("narrow", (0, 1, 2), ()), (2, 6)),
181 (lambda t: t.view(6, 4), ("view", (6, 4), ()), (6, 4)),
182 (lambda t: t.reshape(24), ("reshape", (24,), ()), (24,)),
183 (lambda t: t[1], ("__getitem__", (1,), ()), (6,)),
184 (lambda t: t.unsqueeze(0), ("unsqueeze", (0,), ()), (1, 4, 6)),
185 (lambda t: t.transpose(0, 1), ("transpose", (0, 1), ()), (6, 4)),
186 (lambda t: t.t(), ("t", (), ()), (6, 4)),
187 (lambda t: t.permute(1, 0), ("permute", (1, 0), ()), (6, 4)),
188 (lambda t: t.flatten(), ("flatten", (), ()), (24,)),
189 (lambda t: t.contiguous(), ("contiguous", (), ()), (4, 6)),
190 ],
191 )
192 def test_single_return_op_appends_one_spec(self, call, expect_op, expect_shape):
193 child = call(self._fake())
194 assert isinstance(child, FakeRDTTensor)
195 assert child._key() == ("w", (expect_op,))
196 assert tuple(child.shape) == expect_shape
197
198 def test_squeeze_records_its_argument(self):
199 child = self._fake(shape=(1, 4)).squeeze(0)
200 assert child._key() == ("w", (("squeeze", (0,), ()),))
201 assert tuple(child.shape) == (4,)
202
203 def test_chains_compose_in_call_order(self):
204 child = self._fake().t().narrow(0, 0, 3).flatten()
205 assert child._key() == (
206 "w",
207 (
208 ("t", (), ()),
209 ("narrow", (0, 0, 3), ()),
210 ("flatten", (), ()),
211 ),
212 )
213 assert tuple(child.shape) == (12,)
214
215 def test_kwargs_are_frozen_sorted_for_hashability(self):
216 child = self._fake().narrow(dim=0, start=1, length=2)
217 (op, args, kwargs) = child._key()[1][0]
218 assert (op, args) == ("narrow", ())
219 assert kwargs == (("dim", 0), ("length", 2), ("start", 1))
220 hash(child._key()) # the chain is used as a dict key on both sides
221
222 @pytest.mark.parametrize("op,n", [("chunk", 2), ("unbind", 4)])
223 def test_multi_return_op_emits_one_child_per_output(self, op, n):
224 """chunk/unbind hand back a tuple; each child carries the base op plus a
225 trailing __getitem__(i) so the producer can index the replayed result."""
226 parts = getattr(self._fake(), op)(*((n,) if op == "chunk" else ()), 0)
227 assert isinstance(parts, tuple)
228 assert len(parts) == n
229 for i, part in enumerate(parts):
230 base, index = part._key()[1]
231 assert base[0] == op
232 assert index == ("__getitem__", (i,), ())
233
234 def test_op_chain_is_hashable_as_a_fetch_key(self):
235 keys = {self._fake().t()._key(), self._fake().t()._key()}
236 assert len(keys) == 1, "equal chains must collapse — they dedup pull keys"
237
238
239 class TestFakeUnsupportedOps:
240 """Anything that needs real data must fail loudly at bake time rather than
241 silently transferring the wrong bytes."""
242
243 def _fake(self):
244 return FakeRDTTensor(
245 name="w",
246 shape=torch.Size((4,)),
247 dtype=torch.bfloat16,
248 device=META,
249 sink=None,
250 )
251
252 @pytest.mark.parametrize(
253 "call",
254 [
255 lambda t: t.to(torch.float32),
256 lambda t: t.float(),
257 lambda t: t + 1,
258 lambda t: t * 2,
259 lambda t: t.sum(),
260 ],
261 ids=["to", "float", "add", "mul", "sum"],
262 )
263 def test_data_dependent_ops_raise(self, call):
264 with pytest.raises(_UnsupportedFakeOp):
265 call(self._fake())
266
267 def test_error_names_the_weight_and_the_chain(self):
268 with pytest.raises(_UnsupportedFakeOp) as exc:
269 self._fake().narrow(0, 0, 2).float()
270 msg = str(exc.value)
271 assert "'w'" in msg
272 assert "narrow" in msg
273
274 def test_unsupported_is_a_notimplementederror(self):
275 """Callers distinguish "this backend can't handle the loader" from bugs."""
276 assert issubclass(_UnsupportedFakeOp, NotImplementedError)
277
278
279 class TestBakeRecording:
280 """During the dry run the fake's ``copy_`` is the data sink: it records the
281 source chain plus the meta destination's strided region and moves nothing."""
282
283 def _recorder_and_fake(self, shape=(4, 6)):
284 rec = BakeSink()
285 fake = FakeRDTTensor(
286 name="w",
287 shape=torch.Size(shape),
288 dtype=torch.bfloat16,
289 device=META,
290 sink=rec,
291 )
292 return rec, fake
293
294 def test_copy_records_the_destination_region(self):
295 rec, fake = self._recorder_and_fake()
296 layer = _FakeLayer("q_proj")
297 param = torch.empty((8, 6), dtype=torch.bfloat16, device=META)
298 dest = param.narrow(0, 4, 4) # the second half of a fused param
299 rec.current = (layer, "weight")
300 dest.copy_(fake)
301
302 (recorded,) = rec.copies_by_layer[layer]
303 assert recorded.src == ("w", ())
304 assert recorded.param_name == "weight"
305 assert recorded.offset == dest.storage_offset() == 24
306 assert recorded.shape == (4, 6)
307 assert recorded.stride == (6, 1)
308
309 def test_copy_marks_the_source_name_live(self):
310 rec, fake = self._recorder_and_fake()
311 rec.current = (_FakeLayer("l"), "weight")
312 torch.empty((4, 6), dtype=torch.bfloat16, device=META).copy_(fake)
313 assert rec.copied_names == {"w"}
314
315 def test_unattributed_copy_is_live_but_unrecorded(self):
316 """A copy_ with no loader stamp cannot be attributed to a param, so its
317 module must fall back to the plain load — but the name still moved data."""
318 rec, fake = self._recorder_and_fake()
319 rec.current = None
320 torch.empty((4, 6), dtype=torch.bfloat16, device=META).copy_(fake)
321 assert rec.copied_names == {"w"}
322 assert dict(rec.copies_by_layer) == {}
323
324 def test_copies_are_grouped_by_module_in_call_order(self):
325 rec, _ = self._recorder_and_fake()
326 layer = _FakeLayer("gate_up")
327 for i, name in enumerate(("gate", "up")):
328 fake = FakeRDTTensor(
329 name=name,
330 shape=torch.Size((4,)),
331 dtype=torch.bfloat16,
332 device=META,
333 sink=rec,
334 )
335 param = torch.empty((8,), dtype=torch.bfloat16, device=META)
336 rec.current = (layer, "weight")
337 param.narrow(0, 4 * i, 4).copy_(fake)
338 assert [c.src[0] for c in rec.copies_by_layer[layer]] == ["gate", "up"]
339 assert [c.offset for c in rec.copies_by_layer[layer]] == [0, 4]
340
341 def test_recording_a_sliced_source(self):
342 """The chain on the source and the region on the dest are independent."""
343 rec, fake = self._recorder_and_fake(shape=(8, 6))
344 layer = _FakeLayer("k_proj")
345 rec.current = (layer, "weight")
346 param = torch.empty((4, 6), dtype=torch.bfloat16, device=META)
347 param.copy_(fake.narrow(0, 2, 4))
348 (recorded,) = rec.copies_by_layer[layer]
349 assert recorded.src == ("w", (("narrow", (0, 2, 4), ()),))
350 assert recorded.shape == (4, 6)
351
352 def test_the_recorded_dtype_is_the_chains_output_not_the_sources(self):
353 """``view(dtype)`` is allowlisted, so a chain can reinterpret dtype. The
354 producer packs what the replay yields, so the record must carry the
355 POST-chain dtype: taking it from the source name's metadata instead sizes
356 the slice with the wrong itemsize and shifts every later slice in the
357 chunk, carving the packed blob differently on the two sides."""
358 rec, fake = self._recorder_and_fake(shape=(4,))
359 layer = _FakeLayer("reinterpreted")
360 rec.current = (layer, "weight")
361 viewed = fake.view(torch.float32) # 4 x bf16 -> 2 x f32
362 param = torch.empty((2,), dtype=torch.float32, device=META)
363 param.copy_(viewed)
364
365 (recorded,) = rec.copies_by_layer[layer]
366 assert recorded.dtype is torch.float32, "recorded the source dtype"
367 assert recorded.shape == (2,)
368 # What the source name alone would have said, and what the producer
369 # would NOT have sent.
370 assert 2 * torch.float32.itemsize == 8 != 2 * torch.bfloat16.itemsize
371
372 def test_a_broadcasting_copy_is_refused_at_bake(self):
373 """The packed layout sizes each slice from the DESTINATION shape while
374 the producer packs what the chain yields, and nothing downstream can
375 catch a mismatch — the consumer's buffer view is exactly prod(dest.shape)
376 elements, so it reshapes cleanly over bytes laid out at other offsets.
377 So the bake refuses rather than recording a slice it cannot carve."""
378 rec, fake = self._recorder_and_fake(shape=(1, 6))
379 rec.current = (_FakeLayer("broadcast"), "weight")
380 param = torch.empty((4, 6), dtype=torch.bfloat16, device=META)
381 with pytest.raises(_UnsupportedFakeOp, match="broadcasting"):
382 param.copy_(fake)
383
384
385 # ---------------------------------------------------------------------------
386 # Consumer identity
387 # ---------------------------------------------------------------------------
388
389
390 class TestConsumerIdentity:
391 """Every worker in the fleet needs a DISTINCT id in 0..C-1: it selects the
392 worker's producer block and keys the producer's per-consumer serve ring, so
393 a collision silently serves two workers out of one ring."""
394
395 def _engine(self, *, dp_index, rank, world_size):
396 eng = object.__new__(ShardedRDTWeightTransferEngine)
397 eng.parallel_config = SimpleNamespace(
398 data_parallel_index=dp_index, rank=rank, world_size=world_size
399 )
400 return eng
401
402 def _ids(self, *, num_consumers, workers, replica_rank=0, num_replicas=1):
403 """Consumer ids for a DP-only engine whose workers are ``dp_index``es."""
404 info = ShardedRDTWeightTransferInitInfo(
405 num_consumers=num_consumers,
406 replica_rank=replica_rank,
407 num_replicas=num_replicas,
408 )
409 out = []
410 for dp_index in workers:
411 eng = self._engine(dp_index=dp_index, rank=0, world_size=1)
412 eng._num_consumers_override = num_consumers
413 out.append(eng._resolve_consumer_id(info))
414 return out
415
416 def test_one_engine_indexes_by_dp_and_tp(self):
417 """dense-via-TP and MoE-via-DP+EP both flatten to 0..C-1."""
418 eng = self._engine(dp_index=0, rank=3, world_size=8)
419 eng._num_consumers_override = 8
420 info = ShardedRDTWeightTransferInitInfo(num_consumers=8)
421 assert eng._resolve_consumer_id(info) == 3
422
423 eng = self._engine(dp_index=5, rank=0, world_size=1)
424 eng._num_consumers_override = 8
425 assert eng._resolve_consumer_id(info) == 5
426
427 def test_independent_engines_offset_into_distinct_ranges(self):
428 """A fleet of separate engines restarts _global_worker_index at 0 in
429 each, so without the replica offset every engine would claim 0..w-1.
430 The driver sets replica_rank on the payload; these are the ids it buys."""
431 ids = [
432 self._ids(num_consumers=8, workers=range(4), replica_rank=r, num_replicas=2)
433 for r in (0, 1)
434 ]
435 assert ids == [[0, 1, 2, 3], [4, 5, 6, 7]]
436 assert len(set(ids[0] + ids[1])) == 8, "every worker distinct fleet-wide"
437
438 def test_the_default_is_a_single_engine_with_no_offset(self):
439 assert self._ids(num_consumers=4, workers=range(4)) == [0, 1, 2, 3]
440
441
442 # ---------------------------------------------------------------------------
443 # RdtRouter: per-name ownership -> one producer per pull
444 # ---------------------------------------------------------------------------
445
446
447 def _router(
448 num_producers,
449 num_consumers,
450 owner_sets=None,
451 name_owner_class=None,
452 names=None,
453 group_lens=None,
454 ):
455 """A router over ``n`` single-name groups unless told otherwise, so a test
456 can state expectations in group indices."""
457 if names is None:
458 n = len(name_owner_class or []) or (len(group_lens or []) or 1)
459 names = [f"g{i}" for i in range(n)]
460 if group_lens is None:
461 group_lens = [1] * len(names)
462 return RdtRouter(
463 num_producers, num_consumers, owner_sets, name_owner_class, names, group_lens
464 )
465
466
467 class TestRdtRouter:
468 """Who serves each name.
469
470 A wrong answer is not a wrong number but a hang or a loud misroute: a
471 consumer pulling from a producer that never gathered the name trips its
472 served-names guard. Freeing does NOT route this way — every consumer signals
473 every owner of a group and the producers count to the live total — so these
474 pin PULL routing, plus ``group_owners`` for the free fan-out.
475 """
476
477 def test_identity_when_fleets_match(self):
478 r = _router(8, 8, group_lens=[1] * 6)
479 assert all(r.producer_for(3, f"g{g}") == 3 for g in range(6))
480 assert all(r.producer_for(c, "g0") == c for c in range(8))
481
482 def test_every_producer_nic_carries_traffic(self):
483 """16 producers / 8 consumers: the block rule spreads each consumer's
484 pulls over its block, alternating by group, so no producer sits idle.
485 Plain `consumer_id % len(owners)` would idle half of them."""
486 r = _router(16, 8, group_lens=[1] * 95)
487 names = [f"g{i}" for i in range(95)]
488 for c in range(8):
489 block = assign_producer_indices(16, 8, c)
490 assert {r.producer_for(c, n) for n in names} == set(block)
491 assert [r.producer_for(0, f"g{g}") for g in range(4)] == [0, 1, 0, 1]
492 assert {r.producer_for(c, n) for c in range(8) for n in names} == set(range(16))
493
494 def test_fan_in_shares_one_producer(self):
495 r = _router(2, 8, group_lens=[1] * 5)
496 names = [f"g{i}" for i in range(5)]
497 assert [{r.producer_for(c, n) for n in names} for c in range(8)] == [
498 {c // 4} for c in range(8)
499 ]
500
501 def test_pipeline_stages_route_to_the_owning_stage(self):
502 """2 stages x 8 ranks: groups 0-2 on stage 0, 3-5 on stage 1. Two owner
503 sets, one per stage; each consumer reaches both."""
504 r = _router(
505 16,
506 8,
507 owner_sets=[list(range(8)), list(range(8, 16))],
508 name_owner_class=[0, 0, 0, 1, 1, 1],
509 )
510 for c in range(8):
511 picks = [r.producer_for(c, f"g{g}") for g in range(6)]
512 assert picks == [c] * 3 + [c + 8] * 3
513 assert r.group_owners(0) == list(range(8))
514 assert r.group_owners(3) == list(range(8, 16))
515 r.validate()
516
517 def test_expert_names_route_to_the_holding_ranks(self):
518 """A name held by only some ranks routes to those; a replicated name
519 keeps the full set."""
520 r = RdtRouter(
521 8,
522 4,
523 [[0, 1, 2, 3], [2, 3], [4, 5, 6, 7], [6, 7]],
524 [0, 1, 2, 3],
525 ["repl0", "expert0", "repl1", "expert1"],
526 [2, 2],
527 )
528 r.validate()
529 assert r.owners("expert0") == [2, 3]
530 assert r.owners("expert1") == [6, 7]
531 assert r.owners("repl1") == [4, 5, 6, 7]
532 for c in range(4):
533 assert r.producer_for(c, "expert0") in (2, 3)
534 assert r.producer_for(c, "repl1") in (4, 5, 6, 7)
535
536 def test_consumers_spread_over_an_owner_set(self):
537 """Several ranks hold a name (its TP peers); the block rule must spread
538 consumers across them, not funnel through one."""
539 r = _router(
540 4, 4, owner_sets=[[0, 1]], name_owner_class=[0] * 6, group_lens=[1] * 6
541 )
542 served = {r.producer_for(c, f"g{g}") for c in range(4) for g in range(6)}
543 assert served == {0, 1}
544
545 def test_a_group_frees_at_every_owner_of_any_of_its_names(self):
546 """The free barrier is per group, so its fan-out is the UNION over the
547 group's names: a rank holding none of them is not signalled."""
548 r = RdtRouter(4, 2, [[0, 1], [2, 3]], [0, 1, 0], ["a", "b", "c"], [2, 1])
549 assert r.group_owners(0) == [0, 1, 2, 3]
550 assert r.group_owners(1) == [0, 1]
551
552 def test_an_unowned_name_raises(self):
553 r = RdtRouter(2, 2, [[]], [0], ["w"], [1])
554 with pytest.raises(ValueError, match="has no owner"):
555 r.producer_for(0, "w")
556
557 def test_validate_rejects_an_empty_owner_set(self):
558 with pytest.raises(ValueError, match="empty"):
559 RdtRouter(
560 4, 2, [[0, 1], [], [2, 3]], [0, 1, 2], ["a", "b", "c"], [3]
561 ).validate()
562
563 def test_validate_rejects_an_out_of_range_owner(self):
564 with pytest.raises(ValueError, match="out of range"):
565 RdtRouter(2, 2, [[0, 5]], [0], ["a"], [1]).validate()
566
567 def test_validate_rejects_an_unresolvable_class(self):
568 with pytest.raises(ValueError, match="owner class"):
569 RdtRouter(4, 2, [[0, 1]], [0, 7], ["a", "b"], [2]).validate()
570
571
572 class TestReplicaOverlay:
573 """[RDT-SHARE-SLOTS] Several inference deployments carve the same blocks.
574
575 The producer can serve R deployments out of ONE slot only if the R copies of
576 a worker meet on the same producer asking for the same bytes. Identical
577 plans give the second half for free; this is the first half. Carving over
578 the whole fleet instead spreads the multi-owner (replicated) names of
579 different deployments onto different producers, so a producer ends up
580 holding a ring per distinct worker index and nothing can merge.
581 """
582
583 def _fleet(
584 self,
585 num_producers,
586 workers,
587 replicas,
588 owner_sets=None,
589 name_owner_class=None,
590 n_names=6,
591 ):
592 names = [f"g{i}" for i in range(n_names)]
593 return (
594 RdtRouter(
595 num_producers,
596 workers * replicas,
597 owner_sets,
598 name_owner_class,
599 names,
600 [1] * n_names,
601 workers_per_replica=workers,
602 ),
603 names,
604 )
605
606 def test_every_replica_of_a_worker_resolves_the_same_producer(self):
607 r, names = self._fleet(16, 8, 4)
608 for worker in range(8):
609 for name in names:
610 picks = {r.producer_for(worker + rep * 8, name) for rep in range(4)}
611 assert len(picks) == 1, (
612 f"worker {worker} split across producers {picks}"
613 )
614
615 def test_adding_a_deployment_does_not_move_the_first_one(self):
616 """The strongest form: an existing consumer's routes are byte-identical
617 before and after another deployment is provisioned."""
618 one, names = self._fleet(16, 8, 1)
619 two, _ = self._fleet(16, 8, 2)
620 for consumer_id in range(8):
621 assert [one.producer_for(consumer_id, n) for n in names] == [
622 two.producer_for(consumer_id, n) for n in names
623 ]
624
625 def test_one_deployment_is_the_historical_carve(self):
626 """Default (no width) and width == the whole fleet must agree with the
627 plain block rule, so a single-deployment fleet is untouched."""
628 plain = _router(16, 8, group_lens=[1] * 6)
629 explicit, names = self._fleet(16, 8, 1)
630 for consumer_id in range(8):
631 block = assign_producer_indices(16, 8, consumer_id)
632 for name in names:
633 assert plain.producer_for(consumer_id, name) == explicit.producer_for(
634 consumer_id, name
635 )
636 assert explicit.producer_for(consumer_id, name) in block
637
638 def test_the_overlay_holds_for_partially_owned_names(self):
639 """Pipeline stages and experts too: two owner sets, one narrow. The
640 narrow one already forced agreement (one owner, no block to carve); the
641 wide one is what the carve decides."""
642 r, names = self._fleet(
643 8,
644 4,
645 2,
646 owner_sets=[[0, 1, 2, 3], [5]],
647 name_owner_class=[0, 1, 0, 1, 0, 1],
648 )
649 for worker in range(4):
650 for name in names:
651 assert r.producer_for(worker, name) == r.producer_for(worker + 4, name)
652 assert {r.producer_for(c, "g1") for c in range(8)} == {5}
653
654 def test_a_width_that_does_not_divide_the_fleet_raises(self):
655 """A uniform fleet is asserted upstream in ``get_world_size``; if it ever
656 is not, the overlay would map two workers of one deployment onto the same
657 block index, so refuse rather than serve the wrong bytes."""
658 with pytest.raises(ValueError, match="does not divide"):
659 RdtRouter(4, 6, None, None, ["a"], [1], workers_per_replica=4)
660
661 def test_pulls_still_carry_the_global_consumer_id(self):
662 """The carve uses the index WITHIN a deployment, but the wire must keep
663 the fleet-global id: it is what lets the producer tell the sharers of a
664 slot apart, and count their arrivals separately."""
665
666 class _Method:
667 def __init__(self):
668 self.calls = []
669
670 def remote(self, keys, consumer_id, seq):
671 self.calls.append((keys, consumer_id, seq))
672
673 r, _ = self._fleet(4, 2, 2)
674 methods = [_Method() for _ in range(4)]
675 r.bind([object()] * 4, methods, consumer_id=3) # replica 1, worker 1
676 r.pull(2, ["k"], 7)
677
678 assert methods[2].calls == [(["k"], 3, 7)]
679
680
681 # ---------------------------------------------------------------------------
682 # Packed layout: the cross-process invariant
683 # ---------------------------------------------------------------------------
684
685
686 def _producer_pack_offsets(slices):
687 """The producer's rule, transcribed from ``rdt_produce_weights_batched``
688 (sharded_rdt_trainer.py): 16B-aligned offsets in specs order.
689
690 Kept as an independent implementation on purpose: the consumer computing the
691 same offsets is the invariant that makes the packed blob readable, and this
692 is the only thing that catches the two rules drifting apart. A divergence is
693 silent at runtime — the bytes still arrive, they are just carved up wrong.
694 """
695 pack_cur = 0
696 offsets = []
697 for numel, dtype in slices:
698 off = (pack_cur + 15) & ~15
699 pack_cur = off + numel * dtype.itemsize
700 offsets.append(off)
701 return offsets, pack_cur
702
703
704 class TestPackedLayout:
705 def test_consumer_layout_matches_the_producer_rule(self):
706 """Mixed dtypes, sizes that do not land on 16B — i.e. the Kimi-style
707 group (fp8 weights + fp32 scales + bf16 norms) that exposes alignment."""
708 specs = [
709 ("w.fp8", "float8_e4m3fn", [17]),
710 ("w.scale", "float32", [3]),
711 ("w.norm", "bfloat16", [5]),
712 ("w.big", "bfloat16", [1024]),
713 ]
714 baked, name_meta = {}, {}
715 layer = _FakeLayer("mixed")
716 copies = []
717 for name, dtype_name, shape in specs:
718 name_meta[name] = (dtype_name, shape)
719 copies.append(
720 _copy(name, shape=tuple(shape), dtype=_dtype_from_name(dtype_name))
721 )
722 mod_plan = _module(layer, copies)
723 for name, _d, _s in specs:
724 baked[name] = mod_plan
725
726 eng = _planner(baked, name_meta=name_meta)
727 plan = eng._build_call_plan([n for n, _, _ in specs], [len(specs)])
728 (chunk,) = plan.chunks
729
730 want_offsets, want_bytes = _producer_pack_offsets(
731 [(s[0], _dtype_from_name(d)) for _n, d, s in specs]
732 )
733 assert [off for off, _dt, _n, _shape in chunk.pack_layout] == want_offsets
734 assert chunk.pack_bytes == want_bytes
735
736 def test_every_offset_is_16b_aligned(self):
737 specs = [(f"w{i}", "bfloat16", [i * 3 + 1]) for i in range(6)]
738 layer = _FakeLayer("l")
739 copies = [_copy(n, shape=tuple(s)) for n, _d, s in specs]
740 mod_plan = _module(layer, copies)
741 eng = _planner(
742 {n: mod_plan for n, _d, _s in specs},
743 name_meta={n: (d, s) for n, d, s in specs},
744 )
745 (chunk,) = eng._build_call_plan([n for n, _, _ in specs], [len(specs)]).chunks
746 assert all(off % 16 == 0 for off, _dt, _n, _s in chunk.pack_layout)
747
748 def test_layout_carries_dtype_numel_and_shape_for_the_view_carve(self):
749 baked, name_meta, names, group_lens = _one_module_per_layer(1, numel=6)
750 eng = _planner(baked, name_meta=name_meta)
751 chunk = eng._build_call_plan(names, group_lens).chunks[0]
752 (off, dt, numel, shape) = chunk.pack_layout[0]
753 assert (off, dt, numel, shape) == (0, torch.bfloat16, 6, (6,))
754
755 def test_duplicate_source_keys_are_packed_once(self):
756 """Two destinations fed by the SAME (name, chain) pull one slice."""
757 layer = _FakeLayer("l")
758 shared = ("w", ())
759 copies = [
760 _copy("w", "a", shape=(4,)),
761 _copy("w", "b", shape=(4,)),
762 ]
763 assert copies[0].src == shared and copies[1].src == shared
764 eng = _planner(
765 {"w": _module(layer, copies)},
766 name_meta={"w": ("bfloat16", [4])},
767 )
768 (chunk,) = eng._build_call_plan(["w"], [1]).chunks
769 assert len(chunk.scatters) == 2
770 assert chunk.keys == [shared]
771 assert len(chunk.pack_layout) == 1
772
773
774 # ---------------------------------------------------------------------------
775 # Chunk splitting
776 # ---------------------------------------------------------------------------
777
778
779 class TestChunkModuleScatters:
780 """The chunk cut: one chunk per distinct owner class present in the copies,
781 ascending by class index. Derived purely from the bake plus the ownership
782 table, so vLLM's expert placement needs no cases."""
783
784 def test_unstamped_copies_form_one_chunk(self):
785 layer = _FakeLayer("l")
786 copies = [_copy(f"w{i}", shape=(4,)) for i in range(5)]
787 eng = _planner({}, name_meta={f"w{i}": ("bfloat16", [4]) for i in range(5)})
788 chunks = eng._chunk_module_scatters([_module(layer, copies)])
789 assert [ci for ci, _s in chunks] == [0]
790 assert len(chunks[0][1]) == 5
791
792 def test_one_chunk_per_owner_class_ascending(self):
793 """Classes are numbered by first appearance in metadata order, so the
794 replicated class (0) leads and the rest follow — the ordering the old
795 "-1 first, then ascending stamp" rule produced, now falling out of the
796 table instead of a special case."""
797 layer = _FakeLayer("moe")
798 names = ["norm", "e5", "e0", "e2"]
799 copies = [_copy(n, shape=(4,)) for n in names]
800 eng = _planner(
801 {},
802 name_meta={n: ("bfloat16", [4]) for n in names},
803 held_by={"e0": [0], "e2": [1], "e5": [2]},
804 )
805 chunks = eng._chunk_module_scatters([_module(layer, copies)])
806 assert [ci for ci, _s in chunks] == [0, 1, 2, 3]
807 assert [[s.src[0] for s in sc] for _ci, sc in chunks] == [
808 ["norm"],
809 ["e5"],
810 ["e0"],
811 ["e2"],
812 ]
813
814 def test_copy_order_within_a_chunk_is_bake_order(self):
815 """Scatter order within a chunk must follow the bake, not the stamp
816 sort: the packed layout and the producer's replay agree on keys order."""
817 layer = _FakeLayer("moe")
818 names = ["e3", "e1", "e2"] # one stamp, deliberately unsorted names
819 copies = [_copy(n, shape=(4,)) for n in names]
820 eng = _planner(
821 {},
822 name_meta={n: ("bfloat16", [4]) for n in names},
823 held_by={n: [0] for n in names},
824 )
825 ((_ci, scatters),) = eng._chunk_module_scatters([_module(layer, copies)])
826 assert [s.src[0] for s in scatters] == names
827
828 def test_stamps_interleave_across_modules_without_reordering_within(self):
829 """Two modules' copies bucket by stamp; within each bucket the order is
830 module-then-bake order."""
831 a, b = _FakeLayer("a"), _FakeLayer("b")
832 mods = [
833 _module(a, [_copy("a0", shape=(4,)), _copy("a1", shape=(4,))]),
834 _module(b, [_copy("b0", shape=(4,)), _copy("b1", shape=(4,))]),
835 ]
836 eng = _planner(
837 {},
838 name_meta={n: ("bfloat16", [4]) for n in ("a0", "a1", "b0", "b1")},
839 held_by={"a0": [0], "a1": [1], "b0": [0], "b1": [1]},
840 )
841 chunks = eng._chunk_module_scatters(mods)
842 assert [(ci, [s.src[0] for s in sc]) for ci, sc in chunks] == [
843 (1, ["a0", "b0"]),
844 (2, ["a1", "b1"]),
845 ]
846
847 def test_scatters_carry_their_own_dtype(self):
848 """Dtype rides the record from the bake, where it is the fake's dtype
849 AFTER its op chain — not a plan-time lookup of the source name, which
850 would be wrong for any chain that reinterprets dtype."""
851 layer = _FakeLayer("l")
852 eng = _planner({}, name_meta={"w": ("float32", [10])})
853 ((_er, scatters),) = eng._chunk_module_scatters(
854 [_module(layer, [_copy("w", shape=(10,), dtype=torch.float32)])]
855 )
856 (sc,) = scatters
857 assert sc.dtype is torch.float32
858 assert sc.layer is layer
859
860
861 # ---------------------------------------------------------------------------
862 # _build_call_plan
863 # ---------------------------------------------------------------------------
864
865
866 class TestBuildCallPlan:
867 def test_one_chunk_per_group_when_unstamped(self):
868 baked, name_meta, names, group_lens = _one_module_per_layer(3)
869 eng = _planner(baked, name_meta=name_meta)
870 plan = eng._build_call_plan(names, group_lens)
871 assert len(plan.chunks) == len(group_lens) == 5
872 assert plan.pre_free == []
873
874 def test_stamps_multiply_chunks_within_a_group(self):
875 layer = _FakeLayer("l")
876 names = [f"w{i}" for i in range(4)]
877 copies = [_copy(n, shape=(4,)) for n in names]
878 mod_plan = _module(layer, copies)
879 eng = _planner(
880 {n: mod_plan for n in names},
881 name_meta={n: ("bfloat16", [4]) for n in names},
882 held_by={"w2": [0], "w3": [1]},
883 )
884 plan = eng._build_call_plan(names, [4])
885 assert len(plan.chunks) == 3 # replicated, w2's class, w3's class
886
887 def test_materialize_fires_on_a_modules_first_chunk_only(self):
888 """Empty HF params are allocated once per module, by construction —
889 including a FusedMoE-like module whose copies span owner-class chunks."""
890 layer = _FakeLayer("spanning")
891 names = [f"w{i}" for i in range(4)]
892 copies = [_copy(n, shape=(4,)) for n in names]
893 mod_plan = _module(layer, copies)
894 eng = _planner(
895 {n: mod_plan for n in names},
896 name_meta={n: ("bfloat16", [4]) for n in names},
897 held_by={"w0": [0], "w1": [0], "w2": [1], "w3": [1]},
898 )
899 plan = eng._build_call_plan(names, [4])
900 assert [c.materialize for c in plan.chunks] == [[layer], []]
901
902 def test_quant_defers_to_a_modules_last_chunk(self):
903 layer = _FakeLayer("spanning")
904 names = [f"w{i}" for i in range(4)]
905 copies = [_copy(n, shape=(4,)) for n in names]
906 mod_plan = _module(layer, copies)
907 eng = _planner(
908 {n: mod_plan for n in names},
909 name_meta={n: ("bfloat16", [4]) for n in names},
910 held_by={"w0": [0], "w1": [0], "w2": [1], "w3": [1]},
911 )
912 plan = eng._build_call_plan(names, [4])
913 assert [c.quant for c in plan.chunks] == [[], [layer]]
914
915 def test_free_signal_fires_on_each_groups_last_chunk(self):
916 baked, name_meta, names, group_lens = _one_module_per_layer(2)
917 eng = _planner(baked, name_meta=name_meta)
918 plan = eng._build_call_plan(names, group_lens)
919 assert [c.free for c in plan.chunks] == [[0], [1], [2], [3]]
920
921 def test_free_signal_waits_for_a_groups_last_chunk_when_it_is_split(self):
922 """The load-bearing case: with the group cut across owner-class chunks the
923 signal must hang off the LAST one. Signaling earlier lets the producers
924 drop the gather buffers while a later chunk's RDMA is still reading."""
925 layer = _FakeLayer("l")
926 names = [f"w{i}" for i in range(4)]
927 copies = [_copy(n, shape=(4,)) for n in names]
928 mod_plan = _module(layer, copies)
929 eng = _planner(
930 {n: mod_plan for n in names},
931 name_meta={n: ("bfloat16", [4]) for n in names},
932 held_by={"w2": [0], "w3": [1]},
933 )
934 plan = eng._build_call_plan(names, [4])
935 assert len(plan.chunks) == 3
936 assert [c.free for c in plan.chunks] == [[], [], [0]]
937
938 def test_free_signal_timing_across_two_split_groups(self):
939 """Two groups x 2 owner-class chunks each: signals land on each group's
940 last chunk only."""
941 names_a = [f"model.layers.0.w{i}" for i in range(4)]
942 names_b = [f"model.layers.1.w{i}" for i in range(4)]
943 baked, name_meta, stamps = {}, {}, {}
944 for names in (names_a, names_b):
945 layer = _FakeLayer(names[0])
946 mod_plan = _module(layer, [_copy(n, shape=(4,)) for n in names])
947 for i, n in enumerate(names):
948 baked[n] = mod_plan
949 name_meta[n] = ("bfloat16", [4])
950 stamps[n] = [i // 2] # two owner classes per group
951 eng = _planner(baked, name_meta=name_meta, held_by=stamps)
952 plan = eng._build_call_plan(names_a + names_b, [4, 4])
953 assert [c.free for c in plan.chunks] == [[], [0], [], [1]]
954
955 def test_a_group_with_nothing_local_is_signaled_at_sync_start(self):
956 """The owners still published it, so this consumer must signal it —
957 immediately, before the pipeline (signal-before-publish is tolerated),
958 never hung off another group's chunk."""
959 baked, name_meta, names, group_lens = _one_module_per_layer(2)
960 del baked["model.layers.1.w"] # nothing baked, and not live => no pull
961 eng = _planner(
962 baked, name_meta=name_meta, live=set(names) - {"model.layers.1.w"}
963 )
964 plan = eng._build_call_plan(names, group_lens)
965 assert len(plan.chunks) == 3
966 assert plan.pre_free == [2]
967 assert [c.free for c in plan.chunks] == [[0], [1], [3]]
968
969 def test_leading_groups_with_nothing_local_go_to_pre_free(self):
970 baked, name_meta, names, group_lens = _one_module_per_layer(2)
971 del baked["embed.weight"]
972 eng = _planner(baked, name_meta=name_meta, live=set(names) - {"embed.weight"})
973 plan = eng._build_call_plan(names, group_lens)
974 assert plan.pre_free == [0]
975 assert len(plan.chunks) == 3
976
977 def test_modules_are_deduped_within_a_group(self):
978 """Several names of one fused module map to the same scatter list; the
979 plan must not chunk it twice."""
980 layer = _FakeLayer("fused")
981 names = ["qkv.q", "qkv.k", "qkv.v"]
982 copies = [_copy(n, shape=(4,)) for n in names]
983 mod_plan = _module(layer, copies)
984 eng = _planner(
985 {n: mod_plan for n in names},
986 name_meta={n: ("bfloat16", [4]) for n in names},
987 )
988 plan = eng._build_call_plan(names, [3])
989 (chunk,) = plan.chunks
990 assert chunk.materialize == [layer]
991 assert len(chunk.scatters) == 3
992
993 def test_plan_is_a_pure_function_of_its_inputs(self):
994 """Called twice with the same arguments it must produce the same plan —
995 that is what lets the engine build it once and reuse it every sync."""
996 baked, name_meta, names, group_lens = _one_module_per_layer(2)
997 eng = _planner(baked, name_meta=name_meta)
998 a = eng._build_call_plan(names, group_lens)
999 b = eng._build_call_plan(names, group_lens)
1000 assert [c.pack_bytes for c in a.chunks] == [c.pack_bytes for c in b.chunks]
1001 assert [c.keys for c in a.chunks] == [c.keys for c in b.chunks]
1002 assert [c.free for c in a.chunks] == [c.free for c in b.chunks]
1003 assert a.pre_free == b.pre_free
1004
1005 def test_single_producer_routes_every_chunk_to_local_owner_zero(self):
1006 baked, name_meta, names, group_lens = _one_module_per_layer(2)
1007 eng = _planner(baked, name_meta=name_meta)
1008 plan = eng._build_call_plan(names, group_lens)
1009 assert [c.owner for c in plan.chunks] == [0] * len(plan.chunks)
1010
1011
1012 class TestUnbakedNamesGuard:
1013 """A live name with no baked plan has no way to load — there is no fallback,
1014 and its pull would target groups the pipeline already freed — so the plan
1015 build fails at init, naming the names."""
1016
1017 def test_a_live_unbaked_name_fails_the_plan_build(self):
1018 baked, name_meta, names, group_lens = _one_module_per_layer(2)
1019 del baked["norm.weight"] # unbaked but live
1020 eng = _planner(baked, name_meta=name_meta, live=set(names))
1021 with pytest.raises(RuntimeError, match="norm.weight"):
1022 eng._build_call_plan(names, group_lens)
1023
1024 def test_never_copied_names_are_dropped_entirely(self):
1025 """Experts owned by another EP rank no-op in their loader: not an error,
1026 and not a chunk — their group is pre-freed if nothing else fills it."""
1027 baked, name_meta, names, group_lens = _one_module_per_layer(2)
1028 del baked["norm.weight"]
1029 eng = _planner(baked, name_meta=name_meta, live=set(names) - {"norm.weight"})
1030 plan = eng._build_call_plan(names, group_lens)
1031 assert len(names) - 1 == len(plan.chunks)
1032 assert plan.pre_free == [len(group_lens) - 1]
1033
1034
1035 class TestCallPlanRouting:
1036 """Under partial ownership each chunk must be pulled from a producer that
1037 actually holds every name in it. Consumers bind EVERY producer (signals fan
1038 out to all owners), so local index == trainer rank."""
1039
1040 def _routed_planner(
1041 self, held_by_group, num_consumers, consumer_id, *, held_by=None
1042 ):
1043 """``held_by_group[g]`` = the ranks holding group g's names, so a test
1044 can still speak in groups; ``held_by`` overrides individual names."""
1045 baked, name_meta, names, group_lens = _one_module_per_layer(
1046 len(held_by_group) - 2
1047 )
1048 per_name = {n: list(held_by_group[gi]) for gi, n in enumerate(names)}
1049 per_name.update(held_by or {})
1050 n_prod = max(max(o) for o in per_name.values()) + 1
1051 owner_sets: list[list[int]] = []
1052 seen: dict[tuple, int] = {}
1053 classes = []
1054 for n in names:
1055 key = tuple(sorted(per_name[n]))
1056 if key not in seen:
1057 seen[key] = len(owner_sets)
1058 owner_sets.append(list(key))
1059 classes.append(seen[key])
1060 eng = _planner(baked, name_meta=name_meta)
1061 router = RdtRouter(
1062 n_prod, num_consumers, owner_sets, classes, names, group_lens
1063 )
1064 router.validate()
1065 router.bind([object()] * n_prod, [object()] * n_prod, consumer_id)
1066 eng._router = router
1067 return eng, names, group_lens
1068
1069 def test_two_stage_ownership_splits_chunks_between_owners(self):
1070 """Two PP stages, 4 groups: stage 0 holds the first half, stage 1 the
1071 second. Every chunk goes to a holder of its names."""
1072 eng, names, group_lens = self._routed_planner([[0], [0], [1], [1]], 1, 0)
1073 plan = eng._build_call_plan(names, group_lens)
1074 assert [c.owner for c in plan.chunks] == [0, 0, 1, 1]
1075
1076 def test_a_multi_owner_group_still_resolves_to_one_producer(self):
1077 """Every chunk is served by exactly ONE producer: splitting a pull only
1078 multiplies produce calls, since the consumer's NIC bounds it."""
1079 eng, names, group_lens = self._routed_planner([[0, 1]] * 4, 1, 0)
1080 plan = eng._build_call_plan(names, group_lens)
1081 assert all(0 <= c.owner < 2 for c in plan.chunks)
1082
1083 def test_an_expert_name_routes_to_the_rank_holding_it(self):
1084 """PP ∩ EP: stage 0 = ranks 0-1, stage 1 = ranks 2-3, and one expert
1085 name inside a stage-1 group is held by rank 3 alone. The name's own owner
1086 set is what routes it."""
1087 eng, names, group_lens = self._routed_planner(
1088 [[0, 1], [0, 1], [2, 3], [2, 3]],
1089 1,
1090 0,
1091 held_by={"model.layers.1.w": [3]},
1092 )
1093 plan = eng._build_call_plan(names, group_lens)
1094 assert plan.chunks[2].owner == 3
1095 assert plan.chunks[0].owner in (0, 1)
1096 assert plan.chunks[3].owner in (2, 3)
1097
1098 def test_an_unheld_name_is_rejected_at_init(self):
1099 """A name no rank holds can never be served. The trainer raises when it
1100 transposes the holdings; the router's validate() is the consumer-side
1101 backstop, and either way it fails at init rather than at first pull."""
1102 with pytest.raises(ValueError, match="empty"):
1103 RdtRouter(2, 1, [[]], [0], ["w"], [1]).validate()
1104
1105
1106 class TestSignalCompleteness:
1107 """Every gather group is signaled exactly once — after its last chunk when
1108 the worker pulls from it, at sync start otherwise — for ANY expert
1109 placement. A group signaled twice over-credits the barrier; a group never
1110 signaled parks a producer credit and hangs end_sync."""
1111
1112 def _moe_planner(self, worker_experts, *, ep_size=4, n_experts=8, n_layers=2):
1113 """Pre / n_layers MoE layers / post. Each layer: one norm (its own
1114 module, stamp -1) + n_experts expert names stamped ``e // n_local``.
1115 The worker's bake covers only ``worker_experts`` (its placement);
1116 foreign expert names never copied => dropped entirely, as in the real
1117 bake."""
1118 n_local = n_experts // ep_size
1119 names = ["embed.weight"]
1120 group_lens = [1]
1121 baked = {
1122 "embed.weight": _module(
1123 _FakeLayer("embed"), [_copy("embed.weight", shape=(4,))]
1124 )
1125 }
1126 name_meta = {"embed.weight": ("bfloat16", [4])}
1127 stamps: dict = {}
1128 live = {"embed.weight"}
1129 for li in range(n_layers):
1130 norm = f"model.layers.{li}.norm"
1131 enames = [f"model.layers.{li}.experts.{e}.w" for e in range(n_experts)]
1132 names += [norm] + enames
1133 group_lens.append(1 + n_experts)
1134 name_meta[norm] = ("bfloat16", [4])
1135 live.add(norm)
1136 baked[norm] = _module(_FakeLayer(f"norm{li}"), [_copy(norm, shape=(4,))])
1137 fused = _module(
1138 _FakeLayer(f"moe{li}"),
1139 [
1140 _copy(f"model.layers.{li}.experts.{e}.w", shape=(4,))
1141 for e in worker_experts
1142 ],
1143 )
1144 for e in range(n_experts):
1145 n = f"model.layers.{li}.experts.{e}.w"
1146 name_meta[n] = ("bfloat16", [4])
1147 stamps[n] = [e // n_local] # held by that EP rank alone
1148 if e in worker_experts:
1149 baked[n] = fused
1150 live.add(n)
1151 names.append("lm_head.weight")
1152 group_lens.append(1)
1153 baked["lm_head.weight"] = _module(
1154 _FakeLayer("head"), [_copy("lm_head.weight", shape=(4,))]
1155 )
1156 name_meta["lm_head.weight"] = ("bfloat16", [4])
1157 live.add("lm_head.weight")
1158 eng = _planner(baked, name_meta=name_meta, live=live, held_by=stamps)
1159 return eng, names, group_lens
1160
1161 @staticmethod
1162 def _signals(plan):
1163 return sorted(list(plan.pre_free) + [gi for c in plan.chunks for gi in c.free])
1164
1165 def test_linear_placement_signals_every_group_exactly_once(self):
1166 eng, names, group_lens = self._moe_planner(worker_experts=[0, 1, 2, 3])
1167 plan = eng._build_call_plan(names, group_lens)
1168 assert self._signals(plan) == list(range(len(group_lens)))
1169
1170 def test_round_robin_placement_signals_every_group_exactly_once(self):
1171 eng, names, group_lens = self._moe_planner(worker_experts=[0, 2, 4, 6])
1172 plan = eng._build_call_plan(names, group_lens)
1173 assert self._signals(plan) == list(range(len(group_lens)))
1174
1175 def test_placement_changes_only_the_chunk_count_never_the_signals(self):
1176 """Linear experts 0-3 hit 2 owner classes; round_robin 0,2,4,6
1177 hits all 4. More chunks per group, identical signal set."""
1178 plans = {}
1179 for label, experts in (("linear", [0, 1, 2, 3]), ("round_robin", [0, 2, 4, 6])):
1180 eng, names, group_lens = self._moe_planner(worker_experts=experts)
1181 plans[label] = eng._build_call_plan(names, group_lens)
1182 # per MoE group: the replicated chunk + one per owner class present
1183 assert (
1184 len(plans["linear"].chunks) == 2 + 2 * 3
1185 ) # embed+head + 2 layers x (1 + 2)
1186 assert len(plans["round_robin"].chunks) == 2 + 2 * 5 # 2 layers x (1 + 4)
1187 assert self._signals(plans["linear"]) == self._signals(plans["round_robin"])
1188
1189 def test_the_fused_module_materializes_first_and_quants_last_across_chunks(self):
1190 """The FusedMoE shape: one module's copies span every owner-class chunk of
1191 its group; materialize on its first, quant on its last."""
1192 eng, names, group_lens = self._moe_planner(
1193 worker_experts=[0, 2, 4, 6], n_layers=1
1194 )
1195 plan = eng._build_call_plan(names, group_lens)
1196 # group 1 = [norm chunk, coord 0..3 chunks] at chunk indices 1..5
1197 fused_chunks = [
1198 c
1199 for c in plan.chunks[1:6]
1200 if any("experts" in sc.src[0] for sc in c.scatters)
1201 ]
1202 assert len(fused_chunks) == 4
1203 (fused_layer,) = fused_chunks[0].materialize
1204 assert fused_layer.tag == "moe0"
1205 assert fused_chunks[0].quant == []
1206 assert fused_chunks[-1].quant == [fused_layer]
1207
1208
1209 # ---------------------------------------------------------------------------
1210 # Shared helpers
1211 # ---------------------------------------------------------------------------
1212
1213
1214 class TestBufferAllocBytes:
1215 def test_rounds_up_to_a_coarse_256mb_boundary(self):
1216 assert buffer_alloc_bytes(1) == 256 << 20
1217 assert buffer_alloc_bytes(256 << 20) == 256 << 20
1218 assert buffer_alloc_bytes((256 << 20) + 1) == 512 << 20
1219
1220 def test_presize_is_a_floor_not_a_cap(self):
1221 assert buffer_alloc_bytes(1, presize=3 << 30) == 3 << 30
1222 big = 4 << 30
1223 assert buffer_alloc_bytes(big, presize=1 << 30) == big
1224
1225 def test_never_returns_less_than_requested(self):
1226 for nbytes in (1, 1 << 20, (700 << 20) + 3):
1227 assert buffer_alloc_bytes(nbytes) >= nbytes
1228
1229
1230 class TestLayerwiseGroups:
1231 def test_partition_is_one_group_per_layer_between_unindexed_blocks(self):
1232 names = [
1233 "embed.weight",
1234 "model.layers.0.a",
1235 "model.layers.0.b",
1236 "model.layers.1.a",
1237 "norm.weight",
1238 ]
1239 assert layerwise_groups(names) == [
1240 ["embed.weight"],
1241 ["model.layers.0.a", "model.layers.0.b"],
1242 ["model.layers.1.a"],
1243 ["norm.weight"],
1244 ]
1245
1246 def test_flattening_a_partition_returns_the_input_order(self):
1247 """Un-indexed names group by POSITION, not by name class — so one that
1248 looks like a pre-block name still lands after the layers it follows.
1249 Group index therefore means the same thing on every rank."""
1250 names = [
1251 "embed.weight",
1252 "model.layers.0.a",
1253 "embed.weight.tied", # after the layers => its own trailing block
1254 ]
1255 groups = layerwise_groups(names)
1256 assert [n for g in groups for n in g] == names
1257
1258 def test_layers_are_ordered_by_index_not_appearance(self):
1259 names = ["model.layers.10.a", "model.layers.2.a"]
1260 assert layerwise_groups(names) == [
1261 ["model.layers.2.a"],
1262 ["model.layers.10.a"],
1263 ]
1264
1265 def test_stacks_keep_prefix_appearance_order_and_sort_within(self):
1266 """Two stacks stay in the order they appear — the vision tower before the
1267 text stack — while each sorts by index internally."""
1268 names = [
1269 "visual.blocks.1.w",
1270 "visual.blocks.0.w",
1271 "model.language_model.layers.1.w",
1272 "model.language_model.layers.0.w",
1273 ]
1274 assert layerwise_groups(names) == [
1275 ["visual.blocks.0.w"],
1276 ["visual.blocks.1.w"],
1277 ["model.language_model.layers.0.w"],
1278 ["model.language_model.layers.1.w"],
1279 ]
1280
1281 def test_the_post_block_lands_last_however_early_it_arrives(self):
1282 """Megatron-Bridge streams the last pipeline stage's output block before
1283 its layers. Sweeping un-indexed names after the first layer into a
1284 trailing group is what lets the gather loop walk groups ascending."""
1285 names = ["model.layers.0.w", "model.norm.weight", "model.layers.1.w"]
1286 assert layerwise_groups(names) == [
1287 ["model.layers.0.w"],
1288 ["model.layers.1.w"],
1289 ["model.norm.weight"],
1290 ]
1291
1292 def test_names_sharing_a_layer_coalesce_when_the_source_interleaves(self):
1293 """A raw-checkpoint source may yield in shard-packing order. Coalescing on
1294 the key keeps that from shattering one layer into many groups."""
1295 names = [
1296 "model.layers.0.a",
1297 "model.layers.1.a",
1298 "model.layers.0.b",
1299 ]
1300 assert layerwise_groups(names) == [
1301 ["model.layers.0.a", "model.layers.0.b"],
1302 ["model.layers.1.a"],
1303 ]
1304
1305 def test_no_empty_groups(self):
1306 for names in ([], ["only.pre"], ["model.layers.0.a"]):
1307 assert all(g for g in layerwise_groups(names))
1308
1309 def test_a_model_with_no_layers_is_one_pre_group(self):
1310 assert layerwise_groups(["a", "b"]) == [["a", "b"]]
1311
1312 @pytest.mark.parametrize(
1313 "prefix",
1314 [
1315 "model.layers.", # Llama, Qwen3, GLM-4.5, DeepSeek, Mixtral
1316 "model.language_model.layers.", # Qwen3.5/3.6, Qwen2.5-VL, Gemma3
1317 "language_model.layers.", # Kimi-K2.5, Nemotron-VL
1318 "thinker.model.layers.", # Qwen2.5/3-Omni
1319 "transformer.h.", # GPT-2, GPT-J, Bloom, Falcon
1320 "transformer.blocks.", # MPT, DBRX
1321 "transformer.encoder.layers.", # ChatGLM
1322 "backbone.layers.", # Mamba, Mamba2
1323 "gpt_neox.layers.", # GPT-NeoX
1324 "model.decoder.layers.", # OPT
1325 "model.h.", # Exaone
1326 "model.layers.layers.", # Plamo3
1327 "bert.encoder.layer.", # BERT, RoBERTa
1328 "visual.blocks.", # Qwen-VL vision tower
1329 "vision_tower.vision_model.encoder.layers.", # SigLIP/CLIP towers
1330 "audio_tower.layers.", # Whisper-style audio towers
1331 ],
1332 )
1333 def test_every_supported_naming_convention_partitions_per_layer(self, prefix):
1334 """One decoder layer per group under any prefix, with no per-architecture
1335 table. A literal `model.layers.` match silently yields one whole-model
1336 group for most of these."""
1337 names = [f"{prefix}{i}.self_attn.q_proj.weight" for i in range(3)]
1338 assert layerwise_groups(names) == [[n] for n in names]
1339
1340 def test_a_moe_layer_stays_whole(self):
1341 """The OUTERMOST index wins, so per-expert names group by layer. Splitting
1342 on the expert would cut FusedMoE — one leaf module — across groups."""
1343 names = [
1344 "model.layers.0.mlp.experts.0.w1.weight",
1345 "model.layers.0.mlp.experts.1.w1.weight",
1346 "model.layers.1.mlp.experts.0.w1.weight",
1347 ]
1348 assert layerwise_groups(names) == [
1349 [
1350 "model.layers.0.mlp.experts.0.w1.weight",
1351 "model.layers.0.mlp.experts.1.w1.weight",
1352 ],
1353 ["model.layers.1.mlp.experts.0.w1.weight"],
1354 ]
1355
1356 def test_a_vlm_partitions_the_vision_tower_and_the_text_stack(self):
1357 """Qwen-VL layout. Today's literal `model.layers.` match puts every one of
1358 these in ONE group — the whole model — because the text stack ships as
1359 `model.language_model.layers.`."""
1360 names = [
1361 "visual.patch_embed.proj.weight",
1362 "visual.blocks.0.attn.qkv.weight",
1363 "visual.blocks.1.attn.qkv.weight",
1364 "visual.merger.mlp.weight",
1365 "model.language_model.layers.0.self_attn.q_proj.weight",
1366 "lm_head.weight",
1367 ]
1368 assert layerwise_groups(names) == [
1369 ["visual.patch_embed.proj.weight"],
1370 ["visual.blocks.0.attn.qkv.weight"],
1371 ["visual.blocks.1.attn.qkv.weight"],
1372 ["model.language_model.layers.0.self_attn.q_proj.weight"],
1373 ["visual.merger.mlp.weight", "lm_head.weight"],
1374 ]
1375
1376 def test_two_stacks_sharing_an_index_do_not_merge(self):
1377 """The key carries the prefix, so vision block 0 and text layer 0 are
1378 different groups even though both are index 0."""
1379 names = [
1380 "visual.blocks.0.attn.qkv.weight",
1381 "model.language_model.layers.0.self_attn.q_proj.weight",
1382 ]
1383 assert layerwise_groups(names) == [[n] for n in names]
1384
1385
1386 class TestDtypeFromName:
1387 def test_resolves_wire_names(self):
1388 assert _dtype_from_name("bfloat16") is torch.bfloat16
1389 assert _dtype_from_name("float8_e4m3fn") is torch.float8_e4m3fn
1390
1391 def test_rejects_a_non_dtype_attribute(self):
1392 with pytest.raises(ValueError):
1393 _dtype_from_name("nn") # torch.nn exists but is not a dtype
1394
1395 def test_rejects_an_unknown_name(self):
1396 with pytest.raises(ValueError):
1397 _dtype_from_name("float9")
1398
1399
1400 class TestOpAllowlistAgreement:
1401 """The consumer records op chains from ``SUPPORTED_OPS``; the producer
1402 replays a chain only if every op is in ``ALLOWED_OPS``. The two must describe
1403 the same contract, so they are derived from one table.
1404
1405 A divergence is silent until first pull: an op the consumer can emit but the
1406 producer rejects bakes fine at init, then fails a whole sync later with
1407 "disallowed op".
1408 """
1409
1410 def test_the_two_allowlists_describe_the_same_contract(self):
1411 assert set(SUPPORTED_OPS.values()) == set(ALLOWED_OPS)
1412
1413 def test_every_bake_recordable_op_is_replayable(self):
1414 """Anything the bake can record must be serveable, or the failure lands a
1415 whole sync later than the mistake."""
1416 assert not set(SUPPORTED_OPS.values()) - set(ALLOWED_OPS)
1417
1418 def test_the_producer_allows_nothing_the_consumer_cannot_emit(self):
1419 """The allowlist is also a guard against a misbehaving or spoofed
1420 consumer invoking arbitrary methods on trainer tensors, so it must not be
1421 wider than the ops the bake can actually produce. ``to`` in particular
1422 would let a replay change dtype or device."""
1423 assert not set(ALLOWED_OPS) - set(SUPPORTED_OPS.values())
1424 assert "to" not in ALLOWED_OPS
1425
1426 def test_transpose_via_t_is_serveable(self):
1427 """The specific regression: a chain recorded from ``.t()`` must pass the
1428 producer's guard."""
1429 fake = FakeRDTTensor(
1430 name="w",
1431 shape=torch.Size((4, 6)),
1432 dtype=torch.bfloat16,
1433 device=META,
1434 sink=None,
1435 )
1436 (op, _args, _kwargs) = fake.t()._key()[1][0]
1437 assert op in ALLOWED_OPS
1438
1439
1440 # ---------------------------------------------------------------------------
1441 # Bake against a real model (GPU)
1442 # ---------------------------------------------------------------------------
1443
1444 TINY_QWEN = "Qwen/Qwen3-0.6B"
1445
1446 # One small model per attention/FFN shape the bake has to survive. The point is
1447 # the checkpoint->module mapping, which differs structurally between them: GQA
1448 # fuses Q/K/V of unequal head counts into one param, MLA replaces them with the
1449 # compressed kv_a/kv_b pair, and MoE turns per-expert checkpoint entries into
1450 # stacked expert params.
1451 #
1452 # Keep them small: `load_format="dummy"` skips the download but not the
1453 # allocation, so every parameter is still built on the device at full size.
1454 BAKE_MODELS = [
1455 pytest.param(TINY_QWEN, id="gqa-dense"),
1456 pytest.param("TitanML/tiny-mixtral", id="moe"),
1457 # MLA (kv_lora_rank set, so `ModelConfig.use_mla`) plus routed experts. Only
1458 # the STRUCTURE is under test -- `_bake_probe` synthesizes the checkpoint
1459 # from the safetensors headers and never reads a weight value -- so a random
1460 # 0.06B model does the job, as long as it has the MLA head shape the prefill
1461 # backends accept: (qk_nope=128, qk_rope=64, v=128).
1462 #
1463 # Not an fp8 checkpoint: `process_weights_after_loading` swaps the vLLM
1464 # parameter subclasses out on those, and the plain second `load_weights`
1465 # this test compares against then cannot run.
1466 pytest.param("hmellor/tiny-random-DeepseekV2ForCausalLM", id="mla-moe"),
1467 ]
1468
1469
1470 class TestRequiresTheRayExecutor:
1471 """The engine's data plane is Ray's, so a non-Ray executor cannot work. The
1472 check is at construction because the alternative is an opaque failure during
1473 the first handshake, long after the misconfiguration."""
1474
1475 def _construct(self, backend):
1476 cfg = SimpleNamespace(
1477 parallel_config=SimpleNamespace(distributed_executor_backend=backend)
1478 )
1479 eng = object.__new__(ShardedRDTWeightTransferEngine)
1480 # Only the guard is under test, so stand in for the base __init__.
1481 with pytest.MonkeyPatch.context() as m:
1482 m.setattr(
1483 ShardedRDTWeightTransferEngine.__bases__[0],
1484 "__init__",
1485 lambda self, *a, **k: None,
1486 )
1487 ShardedRDTWeightTransferEngine.__init__(eng, None, cfg, META, None)
1488 return eng
1489
1490 @pytest.mark.parametrize("backend", ["uni", "mp", "external_launcher"])
1491 def test_a_non_ray_executor_is_refused(self, backend):
1492 with pytest.raises(ValueError, match="requires distributed_executor"):
1493 self._construct(backend)
1494
1495 def test_ray_is_accepted(self):
1496 self._construct("ray")
1497
1498 def test_a_custom_executor_class_is_left_alone(self):
1499 """A `type[Executor]` override is deliberate and unjudgeable here, so it
1500 must not be rejected for merely not being the string "ray"."""
1501
1502 class _CustomExecutor:
1503 pass
1504
1505 self._construct(_CustomExecutor)
1506
1507
1508 def _bake_probe(self, model_name): # noqa: PLR0915 (runs in the vLLM worker)
1509 """Drive `_bake` against the worker's real model and check the three
1510 properties. Returns a small summary; every assertion fires here, in the
1511 worker, so a failure surfaces as the RPC's exception.
1512
1513 Self-contained on purpose: this is pickled to the worker, so it imports what
1514 it needs rather than closing over this module's globals.
1515 """
1516 import torch
1517 from huggingface_hub import get_safetensors_metadata
1518
1519 from vllm.distributed.weight_transfer.sharded_rdt_common import ALLOWED_OPS
1520 from vllm.distributed.weight_transfer.sharded_rdt_engine import (
1521 ShardedRDTWeightTransferEngine,
1522 ShardedRDTWeightTransferInitInfo,
1523 )
1524 from vllm.model_executor.model_loader.reload.utils import get_layer_tensors
1525
1526 model = self.model_runner.model
1527 device = next(model.parameters()).device
1528
1529 # ---- the checkpoint, exactly as a trainer would describe and serve it ----
1530 # Only the safetensors HEADERS are fetched (a few KB); the values are
1531 # synthesized. Nothing here depends on real weights -- both the reference
1532 # load and the replay read this same dict, so the comparison is unaffected,
1533 # and it keeps a multi-GB download out of CI.
1534 st_dtype = {
1535 "F64": torch.float64,
1536 "F32": torch.float32,
1537 "F16": torch.float16,
1538 "BF16": torch.bfloat16,
1539 "F8_E4M3": torch.float8_e4m3fn,
1540 "F8_E5M2": torch.float8_e5m2,
1541 "I64": torch.int64,
1542 "I32": torch.int32,
1543 "I8": torch.int8,
1544 "U8": torch.uint8,
1545 "BOOL": torch.bool,
1546 }
1547 meta = get_safetensors_metadata(model_name)
1548 specs = {}
1549 for file_meta in meta.files_metadata.values():
1550 for name, t in file_meta.tensors.items():
1551 specs[name] = (st_dtype[t.dtype], tuple(t.shape))
1552
1553 gen = torch.Generator(device="cpu").manual_seed(0)
1554 ckpt: dict[str, torch.Tensor] = {}
1555 for name, (dt, shape) in specs.items():
1556 if dt.is_floating_point:
1557 # Small magnitudes so any narrowing cast stays finite.
1558 v = torch.randn(shape, generator=gen, dtype=torch.float32) * 0.1
1559 ckpt[name] = v.to(dtype=dt)
1560 else:
1561 ckpt[name] = torch.zeros(shape, dtype=dt)
1562 names = sorted(ckpt)
1563 dtype_names = [str(ckpt[n].dtype).split(".")[-1] for n in names]
1564 shapes = [list(ckpt[n].shape) for n in names]
1565
1566 def _feed():
1567 return iter([(n, ckpt[n]) for n in names])
1568
1569 # ---- reference: what the model's OWN loader produces from that checkpoint --
1570 # Poison first, so "which params did load_weights actually write?" is
1571 # answerable. Some params have no checkpoint source at all -- derived ones,
1572 # or ones process_weights_after_loading builds -- and those keep their NaNs
1573 # and are legitimately outside the plan. That set is what the plan must
1574 # cover, not every parameter of the model.
1575 def _poison():
1576 with torch.no_grad():
1577 for prm in model.parameters():
1578 if prm.is_floating_point():
1579 prm.fill_(float("nan"))
1580 else:
1581 prm.zero_()
1582
1583 _poison()
1584 model.load_weights(_feed())
1585 loaded = {
1586 n
1587 for n, prm in model.named_parameters()
1588 if not (prm.is_floating_point() and torch.isnan(prm).any())
1589 }
1590 # On the host: the weights themselves already fill the GPU.
1591 reference = {
1592 n: prm.detach().to("cpu", copy=True)
1593 for n, prm in model.named_parameters()
1594 if n in loaded
1595 }
1596
1597 # ---- pre-bake state, for the restoration check --------------------------
1598 param_name_of = {}
1599 for mod_name, mod in model.named_modules():
1600 for pname in get_layer_tensors(mod):
1601 param_name_of[(id(mod), pname)] = f"{mod_name}.{pname}".lstrip(".")
1602 loaders_before = {}
1603 for mod in model.modules():
1604 for pname, tensor in get_layer_tensors(mod).items():
1605 loaders_before[(id(mod), pname)] = getattr(tensor, "weight_loader", None)
1606
1607 # ---- bake ---------------------------------------------------------------
1608 eng = object.__new__(ShardedRDTWeightTransferEngine)
1609 eng.model = model
1610 eng.device = device
1611 eng._name_to_plan = {}
1612 eng._name_meta = {}
1613 eng._live_names = set()
1614 eng._bake(
1615 ShardedRDTWeightTransferInitInfo(
1616 names=names, dtype_names=dtype_names, shapes=shapes
1617 )
1618 )
1619
1620 # (1) the bake ran and produced a plan
1621 assert eng._name_to_plan, "bake produced no plan"
1622 assert eng._live_names, "bake recorded no live names"
1623 n_modules = len({id(v) for v in eng._name_to_plan.values()})
1624
1625 # (2) every intermediate change to model state was undone
1626 for mod in model.modules():
1627 for pname, tensor in get_layer_tensors(mod).items():
1628 key = (id(mod), pname)
1629 loader = getattr(tensor, "weight_loader", None)
1630 assert not hasattr(loader, "_rdt_stamp_inner"), (
1631 f"recording stamp left on {param_name_of.get(key, key)}"
1632 )
1633 assert loader is loaders_before[key], (
1634 f"weight_loader not restored on {param_name_of.get(key, key)}"
1635 )
1636 assert not tensor.is_meta, (
1637 f"{param_name_of.get(key, key)} left on meta after the dry run"
1638 )
1639 for pname, p in model.named_parameters():
1640 assert torch.equal(p.detach().cpu(), reference[pname]), f"bake mutated {pname}"
1641
1642 # (3) replaying the recorded op chains reproduces that same load
1643 scatters = {id(v): v for v in eng._name_to_plan.values()}
1644
1645 # No deferred-attention layer may be a plan target. The engine's quant
1646 # thread calls `info.reset()` on every module it processes, which makes
1647 # finalize_layerwise_reload SKIP that module -- and finalize is the only
1648 # thing that calls the LAYER's process_weights_after_loading, which is where
1649 # MLA derives W_UK_T/W_UV from kv_b_proj. An attention layer in the plan
1650 # would therefore leave those derived weights stale after a sync, silently.
1651 from vllm.model_executor.layers.attention import is_deferred_attention_layer
1652
1653 attn_in_plan = sorted(
1654 {
1655 type(c.layer).__name__
1656 for plan in scatters.values()
1657 for c in plan
1658 if is_deferred_attention_layer(c.layer)
1659 }
1660 )
1661 assert not attn_in_plan, (
1662 f"plan targets deferred-attention layer(s) {attn_in_plan}; the quant "
1663 f"thread would reset them and finalize would skip their "
1664 f"process_weights_after_loading, staling any derived weights"
1665 )
1666
1667 touched = set()
1668 _poison()
1669 with torch.no_grad():
1670 for plan in scatters.values():
1671 for c in plan:
1672 src_name, chain = c.src
1673 t = ckpt[src_name]
1674 for op, args, kw in chain:
1675 assert op in ALLOWED_OPS, f"disallowed op {op!r}"
1676 t = getattr(t, op)(*args, **dict(kw))
1677 dest = getattr(c.layer, c.param_name)
1678 dest.as_strided(c.shape, c.stride, c.offset).copy_(t)
1679 touched.add(param_name_of[(id(c.layer), c.param_name)])
1680
1681 assert loaded, "the reference load wrote nothing"
1682 # The plan owns everything with a checkpoint source. Anything else the model
1683 # ends up holding is DERIVED -- under MLA, `process_weights_after_loading`
1684 # splits kv_b_proj into W_UK_T/W_UV via `replace_parameter` -- and belongs to
1685 # finalize_layerwise_reload, which `finish_weight_update` runs after the
1686 # replay. So the uncovered set must contain only params owned by a module
1687 # that post-processes; a plain loadable weight going missing still fails.
1688 post_processed = {
1689 id(mod)
1690 for mod in model.modules()
1691 if hasattr(mod, "process_weights_after_loading")
1692 }
1693 modules_by_name = dict(model.named_modules())
1694 uncovered = sorted(loaded - touched)
1695 derived: list[str] = []
1696 unexplained: list[str] = []
1697 for n in uncovered:
1698 mod_name = n.rsplit(".", 1)[0]
1699 mod = modules_by_name.get(mod_name)
1700 is_derived = mod is not None and id(mod) in post_processed
1701 (derived if is_derived else unexplained).append(n)
1702 assert not unexplained, (
1703 f"the plan misses {len(unexplained)} param(s) load_weights wrote that "
1704 f"nothing derives: {unexplained[:5]}"
1705 )
1706 mismatched = [
1707 n
1708 for n, prm in model.named_parameters()
1709 if n in loaded and not torch.equal(prm.detach().cpu(), reference[n])
1710 ]
1711 assert not mismatched, f"replay differs from load_weights for: {mismatched[:5]}"
1712
1713 return {
1714 "names": len(names),
1715 "baked": len(eng._name_to_plan),
1716 "live": len(eng._live_names),
1717 "modules": n_modules,
1718 "loaded_params": len(loaded),
1719 "derived_not_in_plan": len(derived),
1720 "replayed_params": len(touched),
1721 "total_params": sum(1 for _ in model.named_parameters()),
1722 }
1723
1724
1725 @pytest.mark.skipif(
1726 not torch.cuda.is_available(), reason="the bake drives a real model on GPU"
1727 )
1728 class TestBakeOnARealModel:
1729 """`_bake` against a real vLLM model, which the CPU tests above cannot reach:
1730 they drive the planner with hand-built scatters, so nothing there exercises
1731 the dry run through a real `load_weights` — the part that monkeypatches the
1732 model and has to put it back.
1733 """
1734
1735 @pytest.mark.parametrize("model", BAKE_MODELS)
1736 def test_bake_records_a_replayable_plan_and_restores_the_model(
1737 self, vllm_runner, monkeypatch, model
1738 ):
1739 monkeypatch.setenv("VLLM_ALLOW_INSECURE_SERIALIZATION", "1")
1740 with vllm_runner(
1741 model, load_format="dummy", enforce_eager=True, max_model_len=1024
1742 ) as llm:
1743 (summary,) = llm.llm.collective_rpc(_bake_probe, args=(model,))
1744 print(f"bake summary [{model}]: {summary}")
1745 # Non-vacuity: the checks above are only worth something if the plan
1746 # covers the model. Fusion means far fewer leaf modules than names --
1747 # QKV and gate/up for GQA, the stacked experts for MoE.
1748 assert summary["baked"] >= 0.95 * summary["names"], summary
1749 assert summary["live"] >= 0.95 * summary["names"], summary
1750 assert 0 < summary["modules"] < summary["baked"], summary
1751 # The probe already asserts the plan covers every param load_weights
1752 # wrote; this just keeps that from being vacuously few.
1753 assert summary["loaded_params"] > 0.5 * summary["total_params"], summary