| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473 |
- """fast_load slabs: a shard's eager tensors live as views in a few large host buffers (CPU check).
- Runs without CUDA (the slabs are anonymous mmaps then; the packing, views and lifetimes are the
- same code as the pinned path). Checks, on synthetic shards with the checkpoint's expert shapes:
- - ``slab_layout``: every eager tensor placed once, in the loader's (sorted) order within a slab, page-aligned,
- non-overlapping, within the slab size; tensors bigger than a slab get their own buffer.
- - bytes: every rank at EP1 (moe_tp 4) and EP2 (moe_tp 2), target and draft, through the wrapped
- ``safe_open``, equal to the stock ``safe_open`` path (reusing test_fast_load_tp_slice.run_rank),
- for a small slab (many slabs per shard, big tensors standalone), the default and SLAB_MB=0.
- - allocation count: one host buffer per slab (+ one per oversized tensor) instead of one per tensor.
- - lifetimes: every slab is gone once the tensors are dropped; unrequested futures do not pin a slab
- after ``__exit__``; a view the model kept (plain or TP-sliced stand-in) is reported by name by
- ``_release_all`` and its slab frees when it is dropped.
- - pacing + bounded memory: driving the shards like sglang's buffered iterator (max_workers=1) into
- a paced, slow copy pool keeps the budget on tensor bytes and the live slab bytes within the
- loader window + budget + 2 slabs.
- - delayed small copies: the first (small) tensor of every slab copies slowly while its siblings
- finish at once. Each slow copy keeps its whole slab alive; the slab charge keeps the live slabs
- within window + budget + 2 slabs, where the old per-tensor charge let every slab of the load
- stay alive at once (shown with the old charge swapped in).
- - escape after a load: through the real ``load_weights`` wrappers, a view kept past the target load
- is an error naming the tensor (a clean load returns normally, a load that raised keeps its own
- exception); after the draft load it is a warning.
- - memlog: ``DSV41_FAST_LOAD_MEMLOG=1`` logs one line per phase with the allocation counts.
- """
- import collections
- import concurrent.futures
- import gc
- import itertools
- import json
- import os
- import shutil
- import sys
- import tempfile
- import threading
- import time
- import types
- HERE = os.path.dirname(os.path.abspath(__file__))
- sys.path.insert(0, HERE)
- import test_fast_load_tp_slice as tps # noqa: E402 (sets DSV41_FAST_LOAD=1, imports fast_load)
- import safetensors # noqa: E402
- import safetensors.torch as st # noqa: E402
- import torch # noqa: E402
- fl = tps.fl
- MB = 1 << 20
- def set_slab(mb):
- if mb is None:
- os.environ.pop("DSV41_FAST_LOAD_SLAB_MB", None)
- else:
- os.environ["DSV41_FAST_LOAD_SLAB_MB"] = str(mb)
- def all_slabs_expired():
- gc.collect()
- with fl._lock:
- refs = list(fl._slab_refs)
- return [r for r in refs if r[0] is not None and not r[0].expired()]
- def check_layout(files):
- for path in files:
- base, header = fl._parse_header(path)
- for plan in (fl.read_plan(path, "target", (0, 1), 4, (1, 4)), dict.fromkeys(fl.needed_names(path, "target", (0, 2), 4)),
- dict.fromkeys(fl.needed_names(path, "draft"))):
- for slab in (1 * MB, 4 * MB, 256 * MB):
- lay = fl.slab_layout(header, plan, slab)
- order = [m[0] for _, ms in lay for m in ms]
- want = [k for k in sorted(plan) if fl.spec_bytes(header[k], plan[k])[1] > 0]
- assert sorted(order) == want and len(order) == len(set(order)), (path, slab)
- assert all([m[0] for m in ms] == sorted(m[0] for m in ms) for _, ms in lay), (path, slab)
- for nbytes, ms in lay:
- end = 0
- for name, off, kept in ms:
- assert off % fl._SLAB_ALIGN == 0 and off >= end, (name, off, end)
- assert kept == fl.spec_bytes(header[name], plan[name])[1]
- end = off + kept
- assert end <= nbytes
- assert nbytes <= slab or len(ms) == 1, (nbytes, slab, len(ms))
- if nbytes > slab:
- assert ms[0][2] > slab
- assert fl.slab_bytes() == 256 * MB
- for v, want in (("0", 0), ("300", 256 * MB), ("1", MB), ("0.1", MB), ("junk", 256 * MB), ("4096", 4096 * MB)):
- os.environ["DSV41_FAST_LOAD_SLAB_MB"] = v
- assert fl.slab_bytes() == want, (v, fl.slab_bytes())
- set_slab(None)
- print("slab_layout: sorted, page-aligned, non-overlapping, bounded; SLAB_MB parsing rounds to 2^k")
- def count_eager(files, phase, ep, tp):
- """Tensors with bytes that the plan reads for one rank (= host buffers without slabs)."""
- n = 0
- for f in files:
- _, header = fl._parse_header(f)
- plan = fl.read_plan(f, phase, ep if phase == "target" else None, tps.N_TARGET,
- fl.tp_slicing(ep, tp, "auto") if phase == "target" or ep[1] == 1 else None)
- if phase == "target" and fl.tp_slicing(ep, tp, "auto") is None:
- plan = dict.fromkeys(fl.needed_names(f, phase, ep, tps.N_TARGET))
- n += sum(1 for k, sp in plan.items() if fl.spec_bytes(header[k], sp)[1] > 0)
- return n
- def check_bytes_and_counts(files):
- rows = []
- for slab_mb in (4, None, 0):
- set_slab(slab_mb)
- for r in range(4):
- for ep, tp in (((0, 1), (r, 4)), ((r // 2, 2), (r % 2, 2))):
- for phase in ("target", "draft"):
- before = fl._state["host_allocs"], fl._state["slabs"]
- s = tps.run_rank(files, phase, ep, tp, None) # asserts bytes == stock safe_open
- allocs = fl._state["host_allocs"] - before[0]
- slabs = fl._state["slabs"] - before[1]
- left = all_slabs_expired()
- assert not left, (slab_mb, r, ep, phase, [x[3][:2] for x in left])
- n_eager = count_eager(files, phase, ep, tp)
- if slab_mb == 0:
- assert slabs == 0 and allocs == n_eager, (allocs, n_eager)
- else:
- assert slabs == allocs < n_eager, (slabs, allocs, n_eager)
- if r == 0:
- rows.append((slab_mb, "EP1" if ep[1] == 1 else "EP2", phase, n_eager, allocs))
- fl._escaped_slabs() # forget this run's slabs
- set_slab(None)
- for slab_mb, lay, phase, n_eager, allocs in rows:
- print(f" SLAB_MB={slab_mb if slab_mb is not None else '256 (default)':>13} {lay} {phase:6}: "
- f"{n_eager:3d} eager tensors -> {allocs:3d} host buffers")
- print("bytes: EP1 + EP2, 4 ranks, target + draft, SLAB_MB 4 / 256 / 0: equal to stock safe_open; "
- "no slab alive after the tensors are dropped")
- def check_lifetimes(files):
- set_slab(4)
- fl._ep_info, fl._moe_tp_info, fl._state["phase"] = (lambda: (0, 1)), (lambda: (1, 4)), "target"
- os.environ.pop("DSV41_FAST_LOAD_TP_SLICE", None)
- fl._escaped_slabs()
- # 1) only one tensor requested: __exit__ must drop the other futures, so after the kept tensor
- # goes, nothing of the shard stays alive
- with safetensors.safe_open(files[0], framework="pt", device="cpu") as f:
- assert isinstance(f, fl._EagerShard)
- one = f.get_tensor("layers.0.attn.wq_a.weight")
- handle = f
- assert not handle._futures, "unrequested futures survived __exit__"
- time.sleep(0.05)
- alive = all_slabs_expired()
- assert len(alive) == 1, len(alive) # only the slab holding `one`
- del one
- assert not all_slabs_expired()
- del handle
- fl._escaped_slabs()
- # 2) escaped views are reported by name, and free when dropped
- with safetensors.safe_open(files[0], framework="pt", device="cpu") as f:
- got = {k: f.get_tensor(k) for k in f.keys()}
- kept_plain = got["layers.0.attn.wo_a.weight"]
- kept_slice = got["layers.0.ffn.experts.1.w1.scale"] # TP-sliced stand-in
- kept_narrow = got["layers.0.ffn.experts.0.w3.weight"].narrow(0, 576, 576) # the view FusedMoE copies from
- assert isinstance(kept_slice, fl._tp_slice_cls())
- del got
- live, live_bytes, rss, escaped = fl._release_all()
- names = sorted({n for _, _, ns in escaped for n in ns})
- assert names == sorted(["layers.0.attn.wo_a.weight", "layers.0.ffn.experts.1.w1.scale",
- "layers.0.ffn.experts.0.w3.weight"]), names
- assert 1 <= len(escaped) <= 3 and live >= len(escaped), (escaped, live)
- print(f"escape guard: {len(escaped)} slab(s) kept alive by 3 held views reported by name: {names}")
- # the weak references were handed to the report; re-register the survivors through the storage
- from torch.multiprocessing.reductions import StorageWeakRef
- refs = [StorageWeakRef(t.untyped_storage()) for t in (kept_plain, kept_narrow, kept_slice._dsv41_part)]
- del kept_plain, kept_slice, kept_narrow
- gc.collect()
- assert all(r.expired() for r in refs)
- mm = [o for o in gc.get_objects() if isinstance(o, __import__("mmap").mmap) and not o.closed]
- assert not mm, len(mm)
- set_slab(None)
- print("lifetimes: __exit__ drops unrequested futures; slabs free with their last view; nothing left mapped")
- def build_many(d, n_shards=8):
- """n_shards target shards with two routed experts each (layer i), plus one draft shard."""
- files = []
- for i in range(n_shards):
- sd = tps.experts(f"layers.{i}", range(2 * (i % 2), 2 * (i % 2) + 2))
- sd[f"layers.{i}.attn.wq_a.weight"] = tps.rnd((96, 128), torch.bfloat16)
- sd[f"layers.{i}.ffn.gate.weight"] = tps.rnd((4, 5120), torch.bfloat16)
- p = os.path.join(d, f"model-{i + 1:05d}-of-{n_shards:05d}.safetensors")
- st.save_file(sd, p)
- files.append(p)
- with open(os.path.join(d, "config.json"), "w") as f:
- json.dump({"text_config": {"n_routed_experts": tps.N_TARGET}}, f)
- return files
- def buffered_iterator(files, max_workers=1):
- """sglang weight_utils.buffered_multi_thread_safetensors_weights_iterator (image 2026-09-18)."""
- def _load_file(st_file):
- with safetensors.safe_open(st_file, framework="pt", device="cpu") as f:
- return {k: f.get_tensor(k) for k in f.keys()}
- with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as ex:
- it = iter(files)
- pending = collections.deque((p, ex.submit(_load_file, p)) for p in itertools.islice(it, max_workers + 1))
- while pending:
- _, fut = pending.popleft()
- state_dict = fut.result()
- del fut
- nxt = next(it, None)
- if nxt is not None:
- pending.append((nxt, ex.submit(_load_file, nxt)))
- for name in sorted(state_dict.keys()):
- yield name, state_dict[name]
- def check_paced_bound(d):
- files = build_many(d)
- slab = 4 * MB
- set_slab(4)
- fl._ep_info, fl._moe_tp_info, fl._state["phase"] = (lambda: (0, 1)), (lambda: (2, 4)), "target"
- fl._escaped_slabs()
- fl._state["slab_peak_live"] = fl._state["slab_peak_bytes"] = 0
- shard_kept = []
- for p in files:
- _, h = fl._parse_header(p)
- lay = fl.slab_layout(h, fl.read_plan(p, "target", (0, 1), tps.N_TARGET, (2, 4)), slab)
- shard_kept.append(sum(n for n, _ in lay))
- one = 5120 * 288 # a w2 slice, the largest single copy here
- budget = 3 * one
- os.environ["DSV41_FAST_LOAD_INFLIGHT_GB"] = repr(budget / 2**30)
- def orig(*, executor, futures, use_async, func, func_args=(), func_kwargs=None):
- futures.append(executor.submit(func, *func_args, **(func_kwargs or {})))
- mod = types.SimpleNamespace(maybe_executor_submit=orig)
- fl.install_deepseek_v4(mod)
- lock, cur, peak = threading.Lock(), [0], [0]
- param = torch.nn.Parameter(torch.zeros(1), requires_grad=False)
- Slice = fl._tp_slice_cls()
- def copy(p, t):
- n = t._dsv41_resident_nbytes if isinstance(t, Slice) else t.numel() * t.element_size()
- with lock:
- cur[0] += n
- peak[0] = max(peak[0], cur[0])
- src = t.narrow(t._dsv41_dim, t._dsv41_start, t._dsv41_part.shape[t._dsv41_dim]) if isinstance(t, Slice) else t
- src.contiguous().view(torch.uint8).sum() # touch the bytes like the H2D copy would
- time.sleep(0.002)
- with lock:
- cur[0] -= n
- futures = []
- with concurrent.futures.ThreadPoolExecutor(24) as ex:
- for name, t in buffered_iterator(files):
- mod.maybe_executor_submit(executor=ex, futures=futures, use_async=True, func=copy, func_args=(param, t))
- del t
- for fu in concurrent.futures.as_completed(futures):
- fu.result()
- del futures
- peak_live = fl._state["slab_peak_bytes"]
- window = max(sum(shard_kept[i:i + 3]) for i in range(len(shard_kept)))
- assert peak[0] <= budget, (peak[0], budget)
- assert peak_live <= window + budget + 2 * slab, (peak_live, window, budget)
- assert not all_slabs_expired()
- fl._escaped_slabs()
- set_slab(None)
- print(f"paced load over {len(files)} shards: copies in flight peak {peak[0] / 1e6:.1f} MB <= budget "
- f"{budget / 1e6:.1f} MB; live slabs peak {peak_live / 1e6:.1f} MB <= 3-shard window {window / 1e6:.1f} MB "
- f"+ budget + 2 slabs; all slabs freed")
- def build_small(d, n_shards=8, per_shard=64):
- """n_shards shards of per_shard 64 KiB non-expert tensors (16 per 1 MiB slab, 4 slabs a shard)."""
- files = []
- for i in range(n_shards):
- sd = {f"layers.{i}.attn.t{j:03d}.weight": tps.rnd((128, 256), torch.bfloat16) for j in range(per_shard)}
- p = os.path.join(d, f"model-{i + 1:05d}-of-{n_shards:05d}.safetensors")
- st.save_file(sd, p)
- files.append(p)
- with open(os.path.join(d, "config.json"), "w") as f:
- json.dump({"text_config": {"n_routed_experts": tps.N_TARGET}}, f)
- return files
- def delayed_load(files, slow, budget, gate=None, delay=0.15):
- """Paced load where the tensors in ``slow`` copy late: after ``gate`` is set (the old charge,
- which never blocks here) or after ``delay`` seconds. Returns (peak live slab bytes, seconds)."""
- fl._escaped_slabs()
- fl._state["slab_peak_live"] = fl._state["slab_peak_bytes"] = 0
- os.environ["DSV41_FAST_LOAD_INFLIGHT_GB"] = repr(budget / 2**30)
- def orig(*, executor, futures, use_async, func, func_args=(), func_kwargs=None):
- futures.append(executor.submit(func, *func_args, **(func_kwargs or {})))
- mod = types.SimpleNamespace(maybe_executor_submit=orig)
- fl.install_deepseek_v4(mod)
- param = torch.nn.Parameter(torch.zeros(1), requires_grad=False)
- def copy(p, t, name):
- if name in slow:
- if gate is not None:
- assert gate.wait(timeout=30), "gate never opened"
- else:
- time.sleep(delay)
- t.view(torch.uint8).sum()
- t0 = time.time()
- futures = []
- with concurrent.futures.ThreadPoolExecutor(64) as ex:
- for name, t in buffered_iterator(files):
- mod.maybe_executor_submit(executor=ex, futures=futures, use_async=True, func=copy,
- func_args=(param, t, name))
- del t
- if gate is not None:
- gate.set()
- for fu in concurrent.futures.as_completed(futures):
- fu.result()
- del futures
- peak = fl._state["slab_peak_bytes"]
- assert not all_slabs_expired()
- fl._escaped_slabs()
- return peak, time.time() - t0
- def check_delayed_copies(d):
- files = build_small(d)
- slab = 1 * MB
- set_slab(1)
- fl._ep_info, fl._moe_tp_info, fl._state["phase"] = (lambda: (0, 1)), (lambda: (0, 4)), "target"
- slow, shard_kept, n_slabs = set(), [], 0
- for p in files:
- _, h = fl._parse_header(p)
- lay = fl.slab_layout(h, fl.read_plan(p, "target", (0, 1), tps.N_TARGET, (0, 4)), slab)
- assert len(lay) == 4 and all(len(ms) == 16 for _, ms in lay), [len(ms) for _, ms in lay]
- slow |= {ms[0][0] for _, ms in lay} # the first 64 KiB tensor of every slab
- shard_kept.append(sum(n for n, _ in lay))
- n_slabs += len(lay)
- budget = 3 * slab
- window = max(sum(shard_kept[i:i + 3]) for i in range(len(shard_kept)))
- bound = window + budget + 2 * slab
- peak, secs = delayed_load(files, slow, budget)
- assert peak <= bound, (peak, bound)
- # the same load with the old charge (tensor bytes only): nothing stops the loader, so every slab
- # stays alive behind its slow copy
- new_charge = fl._charge
- fl._charge = lambda args: (None, fl._tensor_bytes(args))
- try:
- old_peak, _ = delayed_load(files, slow, budget, gate=threading.Event())
- finally:
- fl._charge = new_charge
- assert old_peak > bound, (old_peak, bound)
- set_slab(None)
- print(f"delayed small copies over {n_slabs} slabs of {slab >> 20} MiB: live slabs peak {peak / MB:.0f} MiB <= "
- f"window {window / MB:.0f} + budget {budget / MB:.0f} + 2 slabs = {bound / MB:.0f} MiB ({secs:.1f} s); "
- f"per-tensor charge: {old_peak / MB:.0f} MiB")
- def check_escape_raises(files):
- set_slab(4)
- fl._ep_info, fl._moe_tp_info, fl._state["phase"] = (lambda: (0, 1)), (lambda: (1, 4)), "target"
- fl._escaped_slabs()
- held = []
- draft_file = next(p for p in files if any(k.startswith("mtp.") for k in fl._parse_header(p)[1]))
- def load(path, keep, fail=False):
- with safetensors.safe_open(path, framework="pt", device="cpu") as f:
- got = {k: f.get_tensor(k) for k in f.keys()}
- if keep:
- held.append(got[keep])
- del got
- if fail:
- raise ValueError("load failed")
- return "loaded"
- class Target:
- def load_weights(self, weights, keep=None, fail=False):
- return load(files[0], keep, fail)
- class Draft:
- def load_weights(self, weights, keep=None):
- return load(draft_file, keep)
- fl._wrap_target_load(types.SimpleNamespace(DeepseekV4ForCausalLM=Target))
- fl.install_dspark(types.SimpleNamespace(DeepseekV4ForCausalLMDSpark=Draft))
- assert Target().load_weights(None) == "loaded"
- try:
- Target().load_weights(None, keep="layers.0.attn.wo_a.weight")
- raise AssertionError("an escaped slab after the target load did not raise")
- except RuntimeError as exc:
- assert "still alive after the target load" in str(exc) and "layers.0.attn.wo_a.weight" in str(exc), exc
- held.clear()
- try:
- Target().load_weights(None, keep="layers.0.attn.wo_a.weight", fail=True)
- raise AssertionError("the load's own exception was lost")
- except ValueError:
- pass
- held.clear()
- mtp = sorted(k for k in fl._parse_header(draft_file)[1] if k.startswith("mtp."))[0]
- assert Draft().load_weights(None, keep=mtp) == "loaded" and fl._state["phase"] == "target"
- held.clear()
- gc.collect()
- assert not all_slabs_expired()
- set_slab(None)
- print("escape after load: target raises naming the tensor, a failed load keeps its exception, draft warns")
- def check_memlog(files):
- """DSV41_FAST_LOAD_MEMLOG=1: one line per phase end with the counters and the allocation counts."""
- import logging
- recs = []
- class H(logging.Handler):
- def emit(self, record):
- recs.append(record.getMessage())
- h = H()
- fl.logger.addHandler(h)
- os.environ["DSV41_FAST_LOAD_MEMLOG"] = "1"
- try:
- fl._ep_info, fl._moe_tp_info, fl._state["phase"] = (lambda: (0, 1)), (lambda: (3, 4)), "target"
- for k in ("host_allocs", "host_alloc_bytes", "slabs", "slab_peak_live", "slab_peak_bytes"):
- fl._state[k] = 0
- fl._mark_load_start()
- with safetensors.safe_open(files[1], framework="pt", device="cpu") as f:
- got = {k: f.get_tensor(k) for k in f.keys()}
- del got
- fl._log_phase("target")
- finally:
- os.environ.pop("DSV41_FAST_LOAD_MEMLOG", None)
- fl.logger.removeHandler(h)
- line = next(r for r in recs if r.startswith("DSV41 fast load memlog: phase=target"))
- summary = next(r for r in recs if "host buffers:" in r)
- for key in ("'pinned_requests': 1", "'slabs': 1", "'slabs_alive_after_release': 0", "'gpu_procs_mib'",
- "'torch_host_alloc'", "'slab_mb': 256"):
- assert key in line, (key, line)
- if os.path.exists("/proc/meminfo"):
- keys = ["'MemTotal'", "'MemAvailable'", "'unaccounted_gib'"] if fl._cuda_mem()[0] is not None else \
- ["'MemTotal'", "'MemAvailable'"]
- if "SecPageTables" in open("/proc/meminfo").read():
- keys.append("'SecPageTables'")
- for key in keys:
- assert key in line, (key, line)
- assert fl._state["host_allocs"] == 0, "phase counters must reset"
- print("memlog: " + line[:160] + " ...")
- print("summary: " + summary[summary.index("host buffers"):][:120])
- def main():
- d = tempfile.mkdtemp(prefix="fl_slab_")
- try:
- files = tps.build(d)
- fl.install_weight_utils(types.SimpleNamespace(safetensors=None))
- assert fl._n_routed_experts(files[0]) == tps.N_TARGET
- check_layout(files)
- check_bytes_and_counts(files)
- check_lifetimes(files)
- d2 = os.path.join(d, "many")
- os.makedirs(d2)
- check_paced_bound(d2)
- d3 = os.path.join(d, "small")
- os.makedirs(d3)
- check_delayed_copies(d3)
- check_escape_raises(files)
- check_memlog(files)
- fl._release_all()
- print("fast_load slab OK")
- finally:
- shutil.rmtree(d, ignore_errors=True)
- if __name__ == "__main__":
- main()
|