Fix RunAI object-storage checkpoint index filtering (#38988)

This commit is contained in:
Aurick Qiao
2026-09-12 12:25:57 +08:00
committed by GitHub
parent a984c78330
commit c389dd0863
4 changed files with 191 additions and 25 deletions
+9 -1
View File
@@ -4118,7 +4118,11 @@ class RunaiModelStreamerLoader(BaseModelLoader):
"""Prepare weights for the model. """Prepare weights for the model.
If the model is not local, it will be downloaded.""" If the model is not local, it will be downloaded."""
from sglang.srt.utils.runai_utils import is_runai_obj_uri, list_safetensors from sglang.srt.utils.runai_utils import (
ObjectStorageModel,
is_runai_obj_uri,
list_safetensors,
)
is_object_storage_path = is_runai_obj_uri(model_name_or_path) is_object_storage_path = is_runai_obj_uri(model_name_or_path)
if self._is_distributed is None: if self._is_distributed is None:
@@ -4160,6 +4164,10 @@ class RunaiModelStreamerLoader(BaseModelLoader):
self.load_config.download_dir, self.load_config.download_dir,
revision, revision,
) )
if is_object_storage_path:
index_file = os.path.abspath(
os.path.join(ObjectStorageModel.get_path(hf_folder), index_file)
)
hf_weights_files = filter_duplicate_safetensors_files( hf_weights_files = filter_duplicate_safetensors_files(
hf_weights_files, hf_folder, index_file hf_weights_files, hf_folder, index_file
) )
+14 -2
View File
@@ -830,7 +830,10 @@ def filter_duplicate_safetensors_files(
if any(fnmatch.fnmatch(rel_path, pattern) for pattern in allow_patterns): if any(fnmatch.fnmatch(rel_path, pattern) for pattern in allow_patterns):
files_to_validate.add(f) files_to_validate.add(f)
missing_files = sorted(f for f in files_to_validate if not os.path.isfile(f)) if "://" in hf_folder:
missing_files = sorted(files_to_validate.difference(hf_weights_files))
else:
missing_files = sorted(f for f in files_to_validate if not os.path.isfile(f))
if missing_files: if missing_files:
raise RuntimeError( raise RuntimeError(
f"{index_file} references {len(missing_files)} shard file(s) missing " f"{index_file} references {len(missing_files)} shard file(s) missing "
@@ -876,7 +879,16 @@ def maybe_add_mtp_safetensors(
# Check if mtp.safetensors exists and is not already in the file list # Check if mtp.safetensors exists and is not already in the file list
mtp_path = os.path.join(hf_folder, "mtp.safetensors") mtp_path = os.path.join(hf_folder, "mtp.safetensors")
if not os.path.isfile(mtp_path) or mtp_path in hf_weights_files: if mtp_path in hf_weights_files:
return hf_weights_files
from sglang.srt.utils.runai_utils import is_runai_obj_uri, list_safetensors
if is_runai_obj_uri(hf_folder):
mtp_exists = mtp_path in list_safetensors(hf_folder)
else:
mtp_exists = os.path.isfile(mtp_path)
if not mtp_exists:
return hf_weights_files return hf_weights_files
# mtp.safetensors exists but not in index - this is a bug # mtp.safetensors exists but not in index - this is a bug
@@ -1,5 +1,8 @@
import concurrent.futures import concurrent.futures
import json
import os
import sys import sys
import tempfile
import threading import threading
import unittest import unittest
from types import SimpleNamespace from types import SimpleNamespace
@@ -20,6 +23,7 @@ from sglang.srt.models.deepseek_v4 import (
_dequant_fp8_wo_a_streaming, _dequant_fp8_wo_a_streaming,
) )
from sglang.srt.models.deepseek_v4_dspark import DeepseekV4ForCausalLMDSpark from sglang.srt.models.deepseek_v4_dspark import DeepseekV4ForCausalLMDSpark
from sglang.srt.utils import runai_utils
from sglang.test.ci.ci_register import register_cpu_ci from sglang.test.ci.ci_register import register_cpu_ci
from sglang.test.test_utils import CustomTestCase from sglang.test.test_utils import CustomTestCase
@@ -32,6 +36,80 @@ class _FakeModel:
class TestRunaiModelStreamerLoader(CustomTestCase): class TestRunaiModelStreamerLoader(CustomTestCase):
def test_remote_checkpoint_index(self):
for scheme in ("s3", "gs", "az"):
for relative_cache in (False, True):
with (
self.subTest(scheme=scheme, relative_cache=relative_cache),
tempfile.TemporaryDirectory() as cache_dir,
):
model_path = f"{scheme}://bucket/model"
shard = "model-00001-of-00001.safetensors"
files = [
f"{model_path}/{shard}",
f"{model_path}/mtp.safetensors",
f"{model_path}/unused.safetensors",
]
cache_root = (
os.path.relpath(cache_dir) if relative_cache else cache_dir
)
with (
patch.object(
runai_utils.envs.SGLANG_CACHE_DIR,
"get",
return_value=cache_root,
),
patch.object(loader_mod, "get_server_args", return_value=None),
patch.object(
runai_utils, "list_safetensors", return_value=files
) as list_safetensors,
patch.object(
weight_utils,
"runai_safetensors_weights_iterator",
return_value=iter(()),
) as streamer,
):
metadata = runai_utils.ObjectStorageModel.get_path(model_path)
os.makedirs(metadata, exist_ok=True)
loader = loader_mod.RunaiModelStreamerLoader(
LoadConfig(load_format=LoadFormat.RUNAI_STREAMER)
)
self.assertEqual(
loader._prepare_weights(model_path, revision=None),
(model_path, files),
)
list_safetensors.assert_called_with(path=model_path)
with open(
os.path.join(metadata, "model.safetensors.index.json"), "w"
) as f:
json.dump({"weight_map": {"weight": shard}}, f)
self.assertEqual(
loader._prepare_weights(model_path, revision=None),
(model_path, [files[0]]),
)
loader.target_device_str = "cpu"
source = loader_mod.RunaiModelStreamerLoader.Source(
model_or_path=model_path,
revision=None,
model_config=cast(
ModelConfig,
SimpleNamespace(
hf_config=SimpleNamespace(
architectures=["Glm4MoeForCausalLMNextN"],
num_nextn_predict_layers=1,
)
),
),
)
list(loader._get_weights_iterator(source))
streamer.assert_called_once_with(files[:2], True, "cpu")
list_safetensors.return_value = [files[-1]]
with self.assertRaisesRegex(RuntimeError, shard):
loader._prepare_weights(model_path, revision=None)
def test_passes_quant_config_to_model_init(self): def test_passes_quant_config_to_model_init(self):
quant_config = object() quant_config = object()
fake_model = _FakeModel() fake_model = _FakeModel()
@@ -4,8 +4,14 @@ import json
import os import os
import tempfile import tempfile
import unittest import unittest
from types import SimpleNamespace
from unittest.mock import patch
from sglang.srt.model_loader.weight_utils import filter_duplicate_safetensors_files from sglang.srt.model_loader.weight_utils import (
filter_duplicate_safetensors_files,
maybe_add_mtp_safetensors,
)
from sglang.srt.utils import runai_utils
from sglang.test.ci.ci_register import register_cpu_ci from sglang.test.ci.ci_register import register_cpu_ci
from sglang.test.test_utils import CustomTestCase from sglang.test.test_utils import CustomTestCase
@@ -85,17 +91,19 @@ class TestFilterDuplicateSafetensorsFiles(CustomTestCase):
"vit": "vision_encoder/model.safetensors", "vit": "vision_encoder/model.safetensors",
}, },
) )
transformer = _touch( shard = "transformer/diffusion_pytorch_model.safetensors"
self.folder, "transformer/diffusion_pytorch_model.safetensors" _touch(self.folder, shard)
)
result = filter_duplicate_safetensors_files( for folder in (self.folder, "s3://bucket/model"):
hf_weights_files=[transformer], with self.subTest(folder=folder):
hf_folder=self.folder, transformer = os.path.join(folder, shard)
index_file=INDEX_NAME, result = filter_duplicate_safetensors_files(
allow_patterns=["transformer/*.safetensors"], hf_weights_files=[transformer],
) hf_folder=folder,
self.assertEqual(result, [transformer]) index_file=os.path.join(self.folder, INDEX_NAME),
allow_patterns=["transformer/*.safetensors"],
)
self.assertEqual(result, [transformer])
def test_missing_shard_inside_allow_patterns_raises(self): def test_missing_shard_inside_allow_patterns_raises(self):
_write_index( _write_index(
@@ -106,18 +114,34 @@ class TestFilterDuplicateSafetensorsFiles(CustomTestCase):
"vit": "vision_encoder/model.safetensors", "vit": "vision_encoder/model.safetensors",
}, },
) )
transformer = _touch( shard = "transformer/model-00001-of-00002.safetensors"
self.folder, "transformer/model-00001-of-00002.safetensors" _touch(self.folder, shard)
)
with self.assertRaises(RuntimeError) as cm: for folder in (self.folder, "s3://bucket/model"):
filter_duplicate_safetensors_files( with (
hf_weights_files=[transformer], self.subTest(folder=folder),
hf_folder=self.folder, self.assertRaisesRegex(
index_file=INDEX_NAME, RuntimeError, r"model-00002-of-00002\.safetensors"
allow_patterns=["transformer/*.safetensors"], ),
) ):
self.assertIn("model-00002-of-00002.safetensors", str(cm.exception)) filter_duplicate_safetensors_files(
hf_weights_files=[os.path.join(folder, shard)],
hf_folder=folder,
index_file=os.path.join(self.folder, INDEX_NAME),
allow_patterns=["transformer/*.safetensors"],
)
def test_local_index_allows_subset_of_existing_shards(self):
_write_index(
self.folder,
{"w1": "first.safetensors", "w2": "second.safetensors"},
)
first = _touch(self.folder, "first.safetensors")
_touch(self.folder, "second.safetensors")
result = filter_duplicate_safetensors_files([first], self.folder, INDEX_NAME)
self.assertEqual(result, [first])
def test_single_file_model_no_index_returns_unchanged(self): def test_single_file_model_no_index_returns_unchanged(self):
# No index on disk (single-file / dummy / object-storage): early return. # No index on disk (single-file / dummy / object-storage): early return.
@@ -131,5 +155,49 @@ class TestFilterDuplicateSafetensorsFiles(CustomTestCase):
self.assertEqual(result, [single]) self.assertEqual(result, [single])
class TestMaybeAddMtpSafetensors(CustomTestCase):
def test_remote_mtp_guards(self):
folder = "s3://bucket/model"
model = f"{folder}/model.safetensors"
mtp = f"{folder}/mtp.safetensors"
cases = (
("Glm4MoeForCausalLM", 1, [model, mtp], [model, mtp], False),
("Glm4MoeForCausalLM", 1, [model], [model], True),
("Glm4MoeForCausalLM", 0, [model], [model, mtp], False),
("LlamaForCausalLM", 1, [model], [model, mtp], False),
)
for arch, nextn, selected, available, may_list in cases:
with (
self.subTest(arch=arch, nextn=nextn, selected=selected),
patch.object(
runai_utils, "list_safetensors", return_value=available
) as listing,
):
config = SimpleNamespace(
architectures=[arch], num_nextn_predict_layers=nextn
)
self.assertEqual(
maybe_add_mtp_safetensors(selected, folder, INDEX_NAME, config),
selected,
)
if not may_list:
listing.assert_not_called()
def test_local_unindexed_mtp(self):
with tempfile.TemporaryDirectory() as folder:
model = _touch(folder, "model.safetensors")
mtp = _touch(folder, "mtp.safetensors")
config = SimpleNamespace(
architectures=["Glm4MoeLiteForCausalLMNextN"],
num_nextn_predict_layers=1,
)
with patch.object(runai_utils, "list_safetensors") as listing:
self.assertEqual(
maybe_add_mtp_safetensors([model], folder, INDEX_NAME, config),
[model, mtp],
)
listing.assert_not_called()
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()