[diffusion] feat: add weight source reader (#35668)

This commit is contained in:
Mick
2026-08-20 18:37:29 +08:00
committed by GitHub
parent 17313cf4b2
commit cf3813f4ce
6 changed files with 321 additions and 47 deletions
@@ -0,0 +1,75 @@
# SPDX-License-Identifier: Apache-2.0
"""Ways of reading checkpoint weights, and the rule for picking one."""
from sglang.multimodal_gen import envs
from sglang.multimodal_gen.runtime.loader.weight_readers.base import (
WeightReader,
)
from sglang.multimodal_gen.runtime.loader.weight_readers.runai_streamer import (
RunaiStreamerReader,
)
from sglang.multimodal_gen.runtime.loader.weight_readers.safetensors_mmap import (
SafetensorsMmapReader,
)
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
logger = init_logger(__name__)
# The fallback is last and always available, so selection cannot come up empty.
_READERS: tuple[type, ...] = (RunaiStreamerReader, SafetensorsMmapReader)
FALLBACK_READER = SafetensorsMmapReader
def available_reader_names() -> list[str]:
return [b.name for b in _READERS if b.is_available()]
def select_weight_reader(
*,
requested: str | None = None,
needs_key_filter: bool = False,
) -> WeightReader:
"""Pick a reader, honouring an explicit request where it can be honoured.
`requested` names a reader; None means take the environment's preference.
A reader that cannot skip keys is passed over when the caller needs to,
because reading the whole checkpoint to discard most of it is worse than
reading the part that was asked for more slowly.
"""
if requested is not None:
chosen = next((b for b in _READERS if b.name == requested), None)
if chosen is None:
raise ValueError(
f"unknown weight reader {requested!r}; "
f"available: {available_reader_names()}"
)
elif envs.SGLANG_USE_RUNAI_MODEL_STREAMER and RunaiStreamerReader.is_available():
chosen = RunaiStreamerReader
else:
chosen = FALLBACK_READER
if not chosen.is_available():
logger.info(
"Weight reader %s is not installed; using %s",
chosen.name,
FALLBACK_READER.name,
)
chosen = FALLBACK_READER
if needs_key_filter and not chosen.supports_key_filter:
logger.debug(
"Weight reader %s cannot skip keys at load time; using %s",
chosen.name,
FALLBACK_READER.name,
)
chosen = FALLBACK_READER
return chosen()
__all__ = [
"FALLBACK_READER",
"RunaiStreamerReader",
"SafetensorsMmapReader",
"WeightReader",
"available_reader_names",
"select_weight_reader",
]
@@ -0,0 +1,47 @@
# SPDX-License-Identifier: Apache-2.0
"""What a weight source has to provide, and what distinguishes one from another.
Reading a checkpoint used to be a boolean: Run:ai streamer, or `safe_open`. The
two differ in more than speed, and the differences decide correctness and memory
behaviour rather than taste, so they are stated here as capabilities:
``supports_key_filter``
Whether the reader can skip keys while reading. The streamer materializes
every tensor before handing any of them over, so a caller that only wants
part of a checkpoint cannot save anything by asking it.
``retains_file_mapping``
Whether the tensors it yields are views into the checkpoint file. Those
pages are file-backed, so the kernel can drop them under pressure without
swap; a reader that copies into anonymous memory gives the kernel nothing
to reclaim.
"""
from typing import Callable, ClassVar, Iterator, Protocol, runtime_checkable
import torch
@runtime_checkable
class WeightReader(Protocol):
"""Yields ``(name, tensor)`` for every weight in a set of checkpoint files."""
name: ClassVar[str]
supports_key_filter: ClassVar[bool]
retains_file_mapping: ClassVar[bool]
@classmethod
def is_available(cls) -> bool:
"""Whether this reader can run at all in this install."""
def iter_weights(
self,
files: list[str],
*,
device: str,
to_cpu: bool,
key_filter: Callable[[str], bool] | None = None,
clone_tensors: bool = True,
show_progress: bool = True,
) -> Iterator[tuple[str, torch.Tensor]]:
"""Iterate the weights, in whatever order the reader finds them."""
@@ -0,0 +1,56 @@
# SPDX-License-Identifier: Apache-2.0
"""Run:ai Model Streamer: fastest to read, but it copies into anonymous memory."""
from typing import Callable, ClassVar, Iterator
import torch
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
logger = init_logger(__name__)
try:
from runai_model_streamer import SafetensorsStreamer
HAS_RUNAI_MODEL_STREAMER = True
except ImportError:
SafetensorsStreamer = None
HAS_RUNAI_MODEL_STREAMER = False
class RunaiStreamerReader:
name: ClassVar[str] = "runai_streamer"
# it materializes every tensor before yielding, so filtering saves nothing
supports_key_filter: ClassVar[bool] = False
retains_file_mapping: ClassVar[bool] = False
@classmethod
def is_available(cls) -> bool:
return HAS_RUNAI_MODEL_STREAMER
def iter_weights(
self,
files: list[str],
*,
device: str,
to_cpu: bool,
key_filter: Callable[[str], bool] | None = None,
clone_tensors: bool = True,
show_progress: bool = True,
) -> Iterator[tuple[str, torch.Tensor]]:
logger.info(
"Loading safetensors with Run:ai Model Streamer to %s",
"cpu" if to_cpu else device,
)
with SafetensorsStreamer() as streamer:
if to_cpu:
streamer.stream_files(files)
else:
streamer.stream_files(files, device=device)
for name, tensor in streamer.get_tensors():
if key_filter is not None and not key_filter(name):
continue
if to_cpu or clone_tensors:
yield name, tensor.clone().detach()
else:
yield name, tensor
@@ -0,0 +1,47 @@
# SPDX-License-Identifier: Apache-2.0
"""`safe_open`: slower to read, and the only source whose pages stay reclaimable.
`safe_open` maps the file, so a CPU tensor it yields is a view into the
checkpoint rather than a copy. Those pages are file-backed, which is what lets
the kernel drop them under memory pressure even on a host with no swap.
"""
from typing import Callable, ClassVar, Iterator
import torch
from safetensors.torch import safe_open
from tqdm.auto import tqdm
_BAR_FORMAT = "{desc}: {percentage:.0f}%|{bar}| {n_fmt}/{total_fmt}"
class SafetensorsMmapReader:
name: ClassVar[str] = "safetensors"
supports_key_filter: ClassVar[bool] = True
retains_file_mapping: ClassVar[bool] = True
@classmethod
def is_available(cls) -> bool:
return True
def iter_weights(
self,
files: list[str],
*,
device: str,
to_cpu: bool,
key_filter: Callable[[str], bool] | None = None,
clone_tensors: bool = True,
show_progress: bool = True,
) -> Iterator[tuple[str, torch.Tensor]]:
for path in tqdm(
files,
desc="Loading safetensors checkpoint shards",
disable=not show_progress,
bar_format=_BAR_FORMAT,
):
with safe_open(path, framework="pt", device=device) as handle:
for name in handle.keys(): # noqa: SIM118
if key_filter is not None and not key_filter(name):
continue
yield name, handle.get_tensor(name)
@@ -18,16 +18,16 @@ from safetensors.torch import safe_open
from torch.distributed.tensor import DTensor
from tqdm.auto import tqdm
try:
from runai_model_streamer import SafetensorsStreamer
HAS_RUNAI_MODEL_STREAMER = True
except ImportError:
HAS_RUNAI_MODEL_STREAMER = False
from sglang.multimodal_gen import envs
from sglang.multimodal_gen.runtime.distributed import get_local_torch_device
from sglang.multimodal_gen.runtime.loader.weight_load_plan import WeightLoadPlan
from sglang.multimodal_gen.runtime.loader.weight_readers import (
FALLBACK_READER,
RunaiStreamerReader,
select_weight_reader,
)
from sglang.multimodal_gen.runtime.loader.weight_readers.runai_streamer import (
HAS_RUNAI_MODEL_STREAMER,
)
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
logger = init_logger(__name__)
@@ -230,14 +230,19 @@ def safetensors_weights_iterator(
device = str(checkpoint_device)
else:
device = "cpu" if to_cpu else str(get_local_torch_device())
if use_runai_model_streamer is None:
use_runai_model_streamer = (
HAS_RUNAI_MODEL_STREAMER and envs.SGLANG_USE_RUNAI_MODEL_STREAMER
# The caller may still pass the old boolean; map it onto a backend name so
# there is one place that decides, and it is the place that knows which
# backends can skip keys.
requested = None
if use_runai_model_streamer is not None:
requested = (
RunaiStreamerReader.name
if use_runai_model_streamer
else FALLBACK_READER.name
)
if key_filter is not None:
# streamer filters after materializing all tensors, so it cannot skip
# a checkpoint partition at load time
use_runai_model_streamer = False
backend = select_weight_reader(
requested=requested, needs_key_filter=key_filter is not None
)
# Validate files before loading
corrupted_files, duplicate_files_by_key = _scan_safetensors_files(hf_weights_files)
@@ -273,38 +278,14 @@ def safetensors_weights_iterator(
_raise_if_duplicate_safetensors_keys(duplicate_files_by_key)
if use_runai_model_streamer:
logger.info(
"Loading safetensors with Run:ai Model Streamer to %s",
"cpu" if to_cpu else device,
)
with SafetensorsStreamer() as streamer:
if to_cpu:
streamer.stream_files(hf_weights_files)
else:
streamer.stream_files(hf_weights_files, device=device)
for name, tensor in streamer.get_tensors():
if key_filter is not None and not key_filter(name):
continue
if to_cpu:
yield name, tensor.clone().detach()
elif clone_streamed_tensors:
yield name, tensor.clone().detach()
else:
yield name, tensor
else:
for st_file in tqdm(
hf_weights_files,
desc="Loading safetensors checkpoint shards",
disable=not enable_tqdm,
bar_format=_BAR_FORMAT,
):
with safe_open(st_file, framework="pt", device=device) as f:
for name in f.keys(): # noqa: SIM118
if key_filter is not None and not key_filter(name):
continue
param = f.get_tensor(name)
yield name, param
yield from backend.iter_weights(
hf_weights_files,
device=device,
to_cpu=to_cpu,
key_filter=key_filter,
clone_tensors=clone_streamed_tensors,
show_progress=enable_tqdm,
)
def _load_pt_file(bin_file: str, device: str) -> dict:
@@ -0,0 +1,68 @@
"""Which backend reads the checkpoint, and why that choice is not a boolean."""
import pytest
from sglang.multimodal_gen.runtime.loader import weight_readers
from sglang.multimodal_gen.runtime.loader.weight_readers import (
FALLBACK_READER,
RunaiStreamerReader,
SafetensorsMmapReader,
available_reader_names,
select_weight_reader,
)
class TestCapabilities:
def test_the_streamer_cannot_skip_keys(self):
# it materializes every tensor before yielding any of them
assert not RunaiStreamerReader.supports_key_filter
def test_only_the_mapping_backend_leaves_pages_reclaimable(self):
assert SafetensorsMmapReader.retains_file_mapping
assert not RunaiStreamerReader.retains_file_mapping
def test_the_fallback_is_always_available(self):
assert FALLBACK_READER.is_available()
assert FALLBACK_READER.name in available_reader_names()
class TestSelection:
def test_an_explicit_request_is_honoured(self):
assert select_weight_reader(requested="safetensors").name == "safetensors"
def test_an_unknown_name_is_an_error_not_a_silent_fallback(self):
with pytest.raises(ValueError, match="unknown weight reader"):
select_weight_reader(requested="does_not_exist")
def test_a_key_filter_passes_over_a_backend_that_cannot_filter(self):
# reading the whole checkpoint to discard most of it is worse than
# reading the requested part more slowly
chosen = select_weight_reader(requested="runai_streamer", needs_key_filter=True)
assert chosen.name == FALLBACK_READER.name
def test_a_key_filter_leaves_a_capable_backend_alone(self):
chosen = select_weight_reader(requested="safetensors", needs_key_filter=True)
assert chosen.name == "safetensors"
def test_an_unavailable_backend_falls_back(self, monkeypatch):
monkeypatch.setattr(
RunaiStreamerReader, "is_available", classmethod(lambda cls: False)
)
assert (
select_weight_reader(requested="runai_streamer").name
== FALLBACK_READER.name
)
def test_the_environment_decides_when_nothing_is_requested(self, monkeypatch):
monkeypatch.setattr(
weight_readers.envs, "SGLANG_USE_RUNAI_MODEL_STREAMER", False
)
assert select_weight_reader().name == FALLBACK_READER.name
def test_the_environment_can_ask_for_the_streamer(self, monkeypatch):
if not RunaiStreamerReader.is_available():
pytest.skip("run:ai model streamer is not installed")
monkeypatch.setattr(
weight_readers.envs, "SGLANG_USE_RUNAI_MODEL_STREAMER", True
)
assert select_weight_reader().name == "runai_streamer"