Support RunAI loading for quantized checkpoints (#23850)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Sam Shleifer <sam@thinkingmachines.ai>
This commit is contained in:
co-authored by
Claude Opus 4.7
Sam Shleifer
parent
321298da75
commit
d41e8c459d
@@ -3238,11 +3238,13 @@ class RunaiModelStreamerLoader(BaseModelLoader):
|
||||
self.target_device_str = "cpu"
|
||||
|
||||
target_device = torch.device(device_config.device)
|
||||
quant_config = _get_quantization_config(model_config, self.load_config)
|
||||
with set_default_torch_dtype(model_config.dtype):
|
||||
with target_device:
|
||||
model = _initialize_model(
|
||||
model_config,
|
||||
self.load_config,
|
||||
quant_config,
|
||||
)
|
||||
|
||||
DefaultModelLoader.load_weights_and_postprocess(
|
||||
@@ -3260,7 +3262,16 @@ def get_model_loader(
|
||||
if load_config.load_format == LoadFormat.DUMMY:
|
||||
return DummyModelLoader(load_config)
|
||||
|
||||
if model_config and (
|
||||
# ModelOptModelLoader's local-copy quantize-and-export workflow doesn't apply
|
||||
# to RUNAI_STREAMER, which streams weights directly from object storage.
|
||||
# RUNAI_STREAMER loads always fall through to the unconditional branch at
|
||||
# the bottom of this function. This also avoids calling _is_already_quantized()
|
||||
# on RunAI streamer cache paths, where huggingface_hub raises HFValidationError.
|
||||
model_optloader_allowed = (
|
||||
model_config and load_config.load_format != LoadFormat.RUNAI_STREAMER
|
||||
)
|
||||
|
||||
if model_optloader_allowed and (
|
||||
(hasattr(model_config, "modelopt_quant") and model_config.modelopt_quant)
|
||||
or model_config.quantization
|
||||
in ["modelopt_fp8", "modelopt_fp4", "modelopt_mixed", "modelopt"]
|
||||
@@ -3270,7 +3281,7 @@ def get_model_loader(
|
||||
|
||||
# Use ModelOptModelLoader for unified quantization flags
|
||||
if (
|
||||
model_config
|
||||
model_optloader_allowed
|
||||
and hasattr(model_config, "quantization")
|
||||
and model_config.quantization
|
||||
in ["modelopt_fp8", "modelopt_fp4", "modelopt_mixed"]
|
||||
|
||||
@@ -69,6 +69,8 @@ except ImportError as e:
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
RUNAI_STREAMER_TENSOR_ATTR = "_sglang_runai_streamer_tensor"
|
||||
|
||||
# Block size for sequential checkpoint prefetch reads (page cache warming).
|
||||
_PREFETCH_BLOCK_SIZE = None
|
||||
|
||||
@@ -1317,7 +1319,9 @@ def runai_safetensors_weights_iterator(
|
||||
mininterval=2,
|
||||
)
|
||||
|
||||
yield from tensor_iter
|
||||
for name, tensor in tensor_iter:
|
||||
setattr(tensor, RUNAI_STREAMER_TENSOR_ATTR, True)
|
||||
yield name, tensor
|
||||
|
||||
|
||||
def set_runai_streamer_env(load_config: LoadConfig):
|
||||
|
||||
@@ -44,7 +44,10 @@ from sglang.srt.model_loader.utils import (
|
||||
should_async_load,
|
||||
should_deepgemm_weight_requant_ue8m0,
|
||||
)
|
||||
from sglang.srt.model_loader.weight_utils import default_weight_loader
|
||||
from sglang.srt.model_loader.weight_utils import (
|
||||
RUNAI_STREAMER_TENSOR_ATTR,
|
||||
default_weight_loader,
|
||||
)
|
||||
from sglang.srt.models.deepseek_common.utils import (
|
||||
_is_cuda,
|
||||
_is_fp8_fnuz,
|
||||
@@ -67,6 +70,12 @@ logger = logging.getLogger(__name__)
|
||||
NVFP4_CKPT_FP8_ATTN_QUANT_MODULES = ["q_b_proj"]
|
||||
|
||||
|
||||
def _clone_if_runai_streamed_tensor(tensor: torch.Tensor) -> torch.Tensor:
|
||||
if getattr(tensor, RUNAI_STREAMER_TENSOR_ATTR, False):
|
||||
return tensor.clone().detach()
|
||||
return tensor
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class NextNEnabledConfig:
|
||||
num_nextn_layers: int
|
||||
@@ -267,7 +276,9 @@ class DeepseekV2WeightLoaderMixin:
|
||||
if fuse_qkv_a_proj and (
|
||||
"q_a_proj" in name or "kv_a_proj_with_mqa" in name
|
||||
):
|
||||
cached_a_proj[name] = loaded_weight
|
||||
cached_a_proj[name] = _clone_if_runai_streamed_tensor(
|
||||
loaded_weight
|
||||
)
|
||||
q_a_proj_name = (
|
||||
name
|
||||
if "q_a_proj" in name
|
||||
|
||||
@@ -743,42 +743,49 @@ class KimiK25ForConditionalGeneration(nn.Module):
|
||||
return hidden_states
|
||||
|
||||
def load_weights(self, weights: Iterable[Tuple[str, torch.Tensor]]):
|
||||
"""Load weights for the model, separating vision and language weights"""
|
||||
"""Stream weights, loading vision weights inline and yielding language weights.
|
||||
|
||||
The streaming pattern (vs accumulating into lists) is required because RunAI's
|
||||
iterator reuses backing buffers — collecting tensors before consuming them
|
||||
would clobber prior tensors.
|
||||
"""
|
||||
mapper = getattr(self, "hf_to_sglang_mapper", None)
|
||||
if mapper is not None:
|
||||
weights = mapper.apply(weights)
|
||||
|
||||
# Separate vision tower weights and language model weights
|
||||
vision_weights = []
|
||||
language_weights = []
|
||||
vision_params = (
|
||||
None
|
||||
if self.config.language_only
|
||||
else dict(self.named_parameters(remove_duplicate=False))
|
||||
)
|
||||
|
||||
for name, loaded_weight in weights:
|
||||
if "vision_tower" in name or "mm_projector" in name:
|
||||
name = name.replace(r"wqkv.", r"attn.qkv_proj.")
|
||||
name = name.replace(r"wo.", r"attn.proj.")
|
||||
name = name.replace("mm_projector.proj.0", "mm_projector.linear_1")
|
||||
name = name.replace("mm_projector.proj.2", "mm_projector.linear_2")
|
||||
vision_weights.append((name, loaded_weight))
|
||||
else:
|
||||
name = name.replace("language_model.", "")
|
||||
# All other weights go to language model
|
||||
language_weights.append((name, loaded_weight))
|
||||
def stream_language_weights():
|
||||
for name, loaded_weight in weights:
|
||||
if "vision_tower" in name or "mm_projector" in name:
|
||||
if vision_params is None:
|
||||
continue
|
||||
vname = (
|
||||
name.replace(r"wqkv.", r"attn.qkv_proj.")
|
||||
.replace(r"wo.", r"attn.proj.")
|
||||
.replace("mm_projector.proj.0", "mm_projector.linear_1")
|
||||
.replace("mm_projector.proj.2", "mm_projector.linear_2")
|
||||
)
|
||||
if vname not in vision_params:
|
||||
raise ValueError(f"Weight {vname} not found in params_dict")
|
||||
param = vision_params[vname]
|
||||
weight_loader = getattr(
|
||||
param, "weight_loader", default_weight_loader
|
||||
)
|
||||
weight_loader(param, loaded_weight)
|
||||
continue
|
||||
yield name.replace("language_model.", ""), loaded_weight
|
||||
|
||||
if not self.config.language_only:
|
||||
# Load vision tower weights
|
||||
vision_state_dict = dict(vision_weights)
|
||||
params_dict = dict(self.named_parameters(remove_duplicate=False))
|
||||
for name, loaded_weight in vision_state_dict.items():
|
||||
if name not in params_dict:
|
||||
raise ValueError(f"Weight {name} not found in params_dict")
|
||||
param = params_dict[name]
|
||||
weight_loader = getattr(param, "weight_loader", default_weight_loader)
|
||||
# loaded_weight = self._pad_vit_attn_dummy_heads(name, loaded_weight)
|
||||
weight_loader(param, loaded_weight)
|
||||
|
||||
# Load language model weights
|
||||
if not self.config.encoder_only and language_weights:
|
||||
self.language_model.load_weights(language_weights)
|
||||
if self.language_model is not None:
|
||||
self.language_model.load_weights(stream_language_weights())
|
||||
else:
|
||||
# encoder-only: drain the generator so inline vision-weight loading fires.
|
||||
for _ in stream_language_weights():
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def get_model_config_for_expert_location(cls, config: KimiK25Config):
|
||||
|
||||
Reference in New Issue
Block a user