Support deepseek v4 and kimi k3 on ssd (#35314)

Co-authored-by: 1BIN4 <1741738350@qq.com>
Co-authored-by: L-Ark <fliangae@connect.ust.hk>
Co-authored-by: Chikati <jxudn@connect.ust.hk>
Co-authored-by: mengzili <zilim@ust.hk>
This commit is contained in:
hujianmin
2026-08-26 10:05:12 +08:00
committed by GitHub
co-authored by 1BIN4 L-Ark Chikati mengzili
parent bec6248272
commit 2d8484740d
46 changed files with 8347 additions and 133 deletions
@@ -0,0 +1,360 @@
"""CUDA unit tests for the MXFP4 expert-pack kernels."""
import unittest
import torch
from sglang.kernels.ops.moe.expert_pack_mxfp4 import (
mxfp4_marlin_repack,
mxfp4_matvec,
mxfp4_matvec_dual,
)
from sglang.test.ci.ci_register import register_cuda_ci
register_cuda_ci(est_time=90, stage="base-b", runner_config="1-gpu-small")
_FP4_VALUES = (
0.0,
0.5,
1.0,
1.5,
2.0,
3.0,
4.0,
6.0,
0.0,
-0.5,
-1.0,
-1.5,
-2.0,
-3.0,
-4.0,
-6.0,
)
_PACK_INDEX = (0, 2, 4, 6, 1, 3, 5, 7)
def _matvec_reference(
input_cpu,
cache_cpu,
slots_cpu,
role_offset,
input_size,
output_size,
records_per_input,
):
blocks = input_size // 32
row_bytes = blocks * 17
result = torch.zeros((slots_cpu.numel(), output_size), dtype=torch.float32)
for record, slot in enumerate(slots_cpu.tolist()):
input_row = record // records_per_input
for output_row in range(output_size):
total = 0.0
row_offset = (
int(slot) * cache_cpu.stride(0) + role_offset + output_row * row_bytes
)
for block in range(blocks):
quant_offset = row_offset + block * 17
scale = 2.0 ** (int(cache_cpu.view(-1)[quant_offset]) - 127)
block_sum = 0.0
for index in range(16):
packed = int(cache_cpu.view(-1)[quant_offset + index + 1])
block_sum += (
float(input_cpu[input_row, block * 32 + index])
* _FP4_VALUES[packed & 0xF]
)
block_sum += (
float(input_cpu[input_row, block * 32 + index + 16])
* _FP4_VALUES[packed >> 4]
)
total += block_sum * scale
result[record, output_row] = total
return result
def _fill_mxfp4_cache(slots, role_bytes, input_size, device):
blocks = input_size // 32
cache_cpu = torch.zeros((slots, 3 * role_bytes), dtype=torch.uint8)
for slot in range(slots):
for role in range(3):
role_rows = role_bytes // (blocks * 17)
for row in range(role_rows):
for block in range(blocks):
offset = (
slot * cache_cpu.stride(0)
+ role * role_bytes
+ row * blocks * 17
+ block * 17
)
cache_cpu.view(-1)[offset] = 126 + ((slot + role + row + block) % 3)
for byte in range(16):
low = (slot + role * 3 + row + block + byte) % 16
high = (15 + slot + role + row + block - byte) % 16
cache_cpu.view(-1)[offset + byte + 1] = low | (high << 4)
return cache_cpu.to(device=device)
def _load_raw_word(raw_cpu, slot, role_offset, row, blocks_per_row, packed_word):
block = packed_word // 4
word_in_block = packed_word & 3
offset = (
slot * raw_cpu.stride(0)
+ role_offset
+ row * blocks_per_row * 17
+ block * 17
+ 1
+ word_in_block * 4
)
word = 0
for byte in range(4):
word |= int(raw_cpu.view(-1)[offset + byte]) << (8 * byte)
return word
def _marlin_nibble(word, value_index):
return (word >> ((value_index & 7) * 4)) & 0xF
def _marlin_scale_perm(index):
local_perm = (0, 2, 1, 3)
interleaved = (index // 4) * 4 + local_perm[index & 3]
return ((interleaved & 7) * 8) + (interleaved >> 3)
def _repack_reference(
raw_cpu, source_slot, role_bytes, input_size, output_size, gate_up
):
blocks = input_size // 32
total_words = (input_size // 16) * output_size * 2
output = torch.empty(total_words, dtype=torch.int32)
tile_span = (output_size // 64) * 128
rows_per_role = output_size // 2 if gate_up else output_size
for index in range(total_words):
tile_k, tile_rem = divmod(index, tile_span)
tile_n, local = divmod(tile_rem, 128)
warp, thread = local & 3, local >> 2
cur_n = warp * 16 + thread // 4
tc_row = (thread & 3) * 2
values = []
for high in (False, True):
source_row = tile_n * 64 + cur_n + (8 if high else 0)
role = (
1 if gate_up and source_row >= rows_per_role else (0 if gate_up else 2)
)
row = source_row % rows_per_role if gate_up else source_row
for offset in (0, 1, 8, 9):
value_index = tc_row + offset
word = _load_raw_word(
raw_cpu,
source_slot,
role * role_bytes,
row,
blocks,
tile_k * 2 + value_index // 8,
)
values.append(_marlin_nibble(word, value_index))
packed = 0
for output_index, value_index in enumerate(_PACK_INDEX):
packed |= values[value_index] << (output_index * 4)
# The CUDA kernel stores the packed uint32 bit pattern in int32.
output[index] = packed if packed < (1 << 31) else packed - (1 << 32)
return output
def _scale_reference(
raw_cpu, source_slot, role_bytes, input_size, output_size, gate_up
):
blocks = input_size // 32
output = torch.empty(blocks * output_size, dtype=torch.uint8)
rows_per_role = output_size // 2 if gate_up else output_size
for index in range(output.numel()):
group, column = divmod(index, output_size)
source_column = (column // 64) * 64 + _marlin_scale_perm(column & 63)
role = (
1 if gate_up and source_column >= rows_per_role else (0 if gate_up else 2)
)
row = source_column % rows_per_role if gate_up else source_column
offset = (
source_slot * raw_cpu.stride(0)
+ role * role_bytes
+ row * blocks * 17
+ group * 17
)
output[index] = raw_cpu.view(-1)[offset]
return output
@unittest.skipUnless(torch.cuda.is_available(), "MXFP4 kernel tests require CUDA")
class TestExpertPackMxfp4(unittest.TestCase):
def test_matvec_fp16_and_bf16(self):
input_size, output_size, records_per_input = 64, 5, 2
blocks = input_size // 32
role_bytes = output_size * blocks * 17
cache = _fill_mxfp4_cache(2, role_bytes, input_size, "cuda")
slots = torch.tensor([1, 0, 1, 0], dtype=torch.int32, device="cuda")
for dtype in (torch.float16, torch.bfloat16):
if dtype is torch.bfloat16 and not torch.cuda.is_bf16_supported():
continue
input_tensor = torch.arange(
2 * input_size, dtype=torch.float32, device="cuda"
).reshape(2, input_size)
input_tensor = ((input_tensor % 19) - 9).to(dtype)
output = mxfp4_matvec(
input_tensor,
cache,
slots,
role_offset=role_bytes,
role_bytes=role_bytes,
input_size=input_size,
output_size=output_size,
records_per_input=records_per_input,
)
reference = _matvec_reference(
input_tensor.cpu(),
cache.cpu(),
slots.cpu(),
role_bytes,
input_size,
output_size,
records_per_input,
).to(dtype)
torch.testing.assert_close(output.cpu(), reference, rtol=0.03, atol=0.25)
def test_matvec_dual_matches_two_roles(self):
input_size, output_size, records_per_input = 64, 17, 2
blocks = input_size // 32
role_bytes = output_size * blocks * 17
cache = _fill_mxfp4_cache(2, role_bytes, input_size, "cuda")
slots = torch.tensor([0, 1, 1, 0], dtype=torch.int32, device="cuda")
input_tensor = (torch.randn(2, input_size, device="cuda") * 0.5).to(
torch.float16
)
output_a, output_b = mxfp4_matvec_dual(
input_tensor,
cache,
slots,
gate_role_offset=0,
up_role_offset=role_bytes,
role_bytes=role_bytes,
input_size=input_size,
output_size=output_size,
records_per_input=records_per_input,
)
expected_a = mxfp4_matvec(
input_tensor,
cache,
slots,
role_offset=0,
role_bytes=role_bytes,
input_size=input_size,
output_size=output_size,
records_per_input=records_per_input,
)
expected_b = mxfp4_matvec(
input_tensor,
cache,
slots,
role_offset=role_bytes,
role_bytes=role_bytes,
input_size=input_size,
output_size=output_size,
records_per_input=records_per_input,
)
torch.testing.assert_close(output_a, expected_a)
torch.testing.assert_close(output_b, expected_b)
def test_marlin_repack_weights_and_scales(self):
hidden_size, intermediate_size = 64, 32
w13_n, w2_n = 2 * intermediate_size, hidden_size
role_bytes = intermediate_size * (hidden_size // 32) * 17
raw_cpu = torch.zeros((4, 3 * role_bytes), dtype=torch.uint8)
for slot in range(4):
for role, rows, size in (
(0, intermediate_size, hidden_size),
(1, intermediate_size, hidden_size),
(2, hidden_size, intermediate_size),
):
groups = size // 32
for row in range(rows):
for group in range(groups):
offset = (
slot * raw_cpu.stride(0)
+ role * role_bytes
+ row * groups * 17
+ group * 17
)
raw_cpu.view(-1)[offset] = (
100 + slot * 7 + role * 11 + row + group
) % 256
for byte in range(16):
raw_cpu.view(-1)[offset + byte + 1] = (
(byte + row + role) % 16
) | (((15 - byte + slot + group) % 16) << 4)
raw = raw_cpu.cuda()
source_slots = torch.tensor([1, 0], dtype=torch.int32, device="cuda")
target_slots = torch.tensor([2, 3], dtype=torch.int32, device="cuda")
w13_words = (hidden_size // 16) * w13_n * 2
w2_words = (intermediate_size // 16) * w2_n * 2
w13_scales = (hidden_size // 32) * w13_n
w2_scales = (intermediate_size // 32) * w2_n
w13 = torch.full((4, w13_words), -1, dtype=torch.int32, device="cuda")
w2 = torch.full((4, w2_words), -1, dtype=torch.int32, device="cuda")
w13_scale = torch.full((4, w13_scales), 255, dtype=torch.uint8, device="cuda")
w2_scale = torch.full((4, w2_scales), 255, dtype=torch.uint8, device="cuda")
mxfp4_marlin_repack(
raw,
source_slots,
target_slots,
role_bytes=role_bytes,
hidden_size=hidden_size,
intermediate_size=intermediate_size,
w13=w13,
w2=w2,
w13_scale=w13_scale,
w2_scale=w2_scale,
)
torch.cuda.synchronize()
for source_slot, target_slot in zip(
source_slots.cpu().tolist(), target_slots.cpu().tolist()
):
expected_w13 = _repack_reference(
raw_cpu, source_slot, role_bytes, hidden_size, w13_n, True
)
expected_w2 = _repack_reference(
raw_cpu, source_slot, role_bytes, intermediate_size, w2_n, False
)
expected_w13_scale = _scale_reference(
raw_cpu, source_slot, role_bytes, hidden_size, w13_n, True
)
expected_w2_scale = _scale_reference(
raw_cpu, source_slot, role_bytes, intermediate_size, w2_n, False
)
torch.testing.assert_close(w13[target_slot].cpu(), expected_w13)
torch.testing.assert_close(w2[target_slot].cpu(), expected_w2)
torch.testing.assert_close(w13_scale[target_slot].cpu(), expected_w13_scale)
torch.testing.assert_close(w2_scale[target_slot].cpu(), expected_w2_scale)
self.assertTrue(torch.all(w13[0] == -1))
self.assertTrue(torch.all(w2[0] == -1))
self.assertTrue(torch.all(w13_scale[0] == 255))
self.assertTrue(torch.all(w2_scale[0] == 255))
def test_invalid_dimensions_are_rejected(self):
input_tensor = torch.zeros((1, 31), dtype=torch.float16, device="cuda")
cache = torch.zeros((1, 17), dtype=torch.uint8, device="cuda")
slots = torch.zeros((1,), dtype=torch.int32, device="cuda")
with self.assertRaisesRegex(RuntimeError, "divisible by 32"):
mxfp4_matvec(
input_tensor,
cache,
slots,
role_offset=0,
role_bytes=17,
input_size=31,
output_size=1,
records_per_input=1,
)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,444 @@
# SPDX-License-Identifier: Apache-2.0
from __future__ import annotations
import hashlib
import json
import sys
import tempfile
import unittest
from dataclasses import replace
from pathlib import Path
import torch
from sglang.test.ci.ci_register import register_cpu_ci
register_cpu_ci(est_time=15, suite="base-a-test-cpu")
ROOT = Path(__file__).resolve().parents[3]
TOOLS = ROOT / "tools" / "expert_pack"
sys.path.insert(0, str(TOOLS))
from format import ( # noqa: E402
ENTRY_STRUCT,
FLAG_IDENTITY_PAYLOAD,
FLAG_TRIPLET_OBJECTS,
HEADER_STRUCT,
IndexEntry,
PackHeader,
align_up,
read_header,
read_index,
)
from sglang.srt.layers.moe.expert_pack import ( # noqa: E402
ExpertPackStore,
_CacheSlot,
)
def _sha256(path: Path) -> str:
return hashlib.sha256(path.read_bytes()).hexdigest()
def _make_pack(directory: Path) -> tuple[Path, Path, dict[str, str]]:
layers, experts, top_k = 1, 2, 1
role_bytes = 17
object_stride = 4096
index_count = layers * experts * 3
data_start = align_up(HEADER_STRUCT.size + index_count * ENTRY_STRUCT.size, 4096)
digests = {
"model_identity": hashlib.sha256(b"model-identity").hexdigest(),
"source": hashlib.sha256(b"source").hexdigest(),
"config": hashlib.sha256(b"config").hexdigest(),
}
header = PackHeader(
flags=FLAG_IDENTITY_PAYLOAD | FLAG_TRIPLET_OBJECTS,
index_count=index_count,
data_start=data_start,
alignment=4096,
num_layers=layers,
num_experts=experts,
top_k=top_k,
role_count=3,
model_identity_sha256=digests["model_identity"],
source_blob_sha256=digests["source"],
config_sha256=digests["config"],
)
entries = []
payloads = []
for expert in range(experts):
object_offset = data_start + expert * object_stride
generation = expert + 100
for role_id, role in enumerate(("gate", "up", "down")):
payload = bytes([expert * 3 + role_id]) * role_bytes
payload_hash = hashlib.sha256(payload).hexdigest()
entries.append(
IndexEntry(
layer=0,
expert=expert,
role=role,
dtype_id=39,
dtype="MXFP4",
tensor_name=f"blk.0.ffn_{role}_exps.weight",
source_tensor_offset=0,
source_tensor_nbytes=role_bytes * experts,
source_slice_offset=expert * role_bytes,
source_slice_nbytes=role_bytes,
pack_offset=object_offset + role_id * role_bytes,
pack_nbytes=role_bytes,
source_tensor_sha256=payload_hash,
source_slice_sha256=payload_hash,
checksum=payload_hash,
shape=(32, 1),
quant_scheme="MXFP4",
transform_id="identity-v1",
block_size=32,
generation=generation,
)
)
payloads.append((object_offset + role_id * role_bytes, payload))
pack = directory / "runtime.expert-pack"
with pack.open("w+b") as stream:
stream.write(header.pack())
for entry in entries:
stream.write(entry.pack())
stream.truncate(data_start + experts * object_stride)
for offset, payload in payloads:
stream.seek(offset)
stream.write(payload)
manifest = directory / "runtime.expert-pack.manifest.json"
manifest.write_text(
json.dumps(
{
"complete": True,
"object_stride": object_stride,
"pack_sha256": _sha256(pack),
}
),
encoding="utf-8",
)
return pack, manifest, digests
def _refresh_manifest_hash(pack: Path, manifest: Path) -> None:
value = json.loads(manifest.read_text(encoding="utf-8"))
value["pack_sha256"] = _sha256(pack)
manifest.write_text(json.dumps(value), encoding="utf-8")
class TestExpertPackRuntime(unittest.TestCase):
def test_victim_prefers_low_frequency_then_lru(self):
store = object.__new__(ExpertPackStore)
keys = [(0, 0), (0, 1), (0, 2)]
store._cache_slots = [
_CacheSlot(key=keys[0], frequency=5),
_CacheSlot(key=keys[1], frequency=1),
_CacheSlot(key=keys[2], frequency=1),
]
store._key_to_slot = {key: index for index, key in enumerate(keys)}
store._lru = dict.fromkeys(keys)
self.assertEqual(store._victim_slot(set()), 1)
self.assertEqual(store._victim_slot({keys[1]}), 2)
self.assertEqual(store._victim_slot(set(), preserve_oldest=True), 2)
with self.assertRaisesRegex(RuntimeError, "active top-k"):
store._victim_slot(set(keys))
def test_zero_staging_slots_are_rejected(self):
with tempfile.TemporaryDirectory() as value:
root = Path(value)
pack, manifest, digests = _make_pack(root)
with self.assertRaisesRegex(ValueError, "staging budgets"):
ExpertPackStore(
pack,
manifest_path=manifest,
expected_layers=1,
expected_experts=2,
expected_top_k=1,
expected_source_sha256=digests["source"],
expected_model_identity_sha256=digests["model_identity"],
expected_config_sha256=digests["config"],
cache_vram_mib=1,
stage_slots=0,
)
def test_full_pack_verification_is_opt_in(self):
with tempfile.TemporaryDirectory() as value:
root = Path(value)
pack, manifest, digests = _make_pack(root)
store = ExpertPackStore(
pack,
manifest_path=manifest,
expected_layers=1,
expected_experts=2,
expected_top_k=1,
expected_source_sha256=digests["source"],
expected_model_identity_sha256=digests["model_identity"],
expected_config_sha256=digests["config"],
cache_vram_mib=1,
stage_slots=1,
)
self.assertEqual(len(store.entries), 6)
self.assertEqual(store.object_payload_bytes, 51)
staging = torch.empty(51, dtype=torch.uint8)
read_bytes, elapsed_ns = store._read_object(0, 1, staging)
self.assertEqual(read_bytes, 51)
self.assertGreaterEqual(elapsed_ns, 0)
self.assertEqual(staging.tolist(), [3] * 17 + [4] * 17 + [5] * 17)
store.close()
with pack.open("r+b") as stream:
stream.seek(-1, 2)
stream.write(b"\x01")
store = ExpertPackStore(
pack,
manifest_path=manifest,
expected_layers=1,
expected_experts=2,
expected_top_k=1,
expected_source_sha256=digests["source"],
expected_model_identity_sha256=digests["model_identity"],
expected_config_sha256=digests["config"],
cache_vram_mib=1,
stage_slots=1,
)
store.close()
with self.assertRaisesRegex(ValueError, "SHA-256"):
ExpertPackStore(
pack,
manifest_path=manifest,
expected_layers=1,
expected_experts=2,
expected_top_k=1,
expected_source_sha256=digests["source"],
expected_model_identity_sha256=digests["model_identity"],
expected_config_sha256=digests["config"],
cache_vram_mib=1,
stage_slots=1,
verify_pack_sha256=True,
)
def test_split_read_ranges_reconstruct_exact_object(self):
with tempfile.TemporaryDirectory() as value:
root = Path(value)
pack, manifest, digests = _make_pack(root)
store = ExpertPackStore(
pack,
manifest_path=manifest,
expected_layers=1,
expected_experts=2,
expected_top_k=1,
expected_source_sha256=digests["source"],
expected_model_identity_sha256=digests["model_identity"],
expected_config_sha256=digests["config"],
cache_vram_mib=1,
stage_slots=1,
)
ranges = store._object_read_ranges()
self.assertEqual(len(ranges), 4)
self.assertEqual(ranges[0][0], 0)
self.assertEqual(sum(length for _, length in ranges), 51)
self.assertTrue(all(length > 0 for _, length in ranges))
self.assertTrue(
all(
ranges[index][0] + ranges[index][1] == ranges[index + 1][0]
for index in range(len(ranges) - 1)
)
)
staging = torch.empty(51, dtype=torch.uint8)
for start, length in ranges:
read_bytes, elapsed_ns = store._read_object_range(
0, 1, staging, start=start, length=length
)
self.assertEqual(read_bytes, length)
self.assertGreaterEqual(elapsed_ns, 0)
self.assertEqual(staging.tolist(), [3] * 17 + [4] * 17 + [5] * 17)
store.close()
def test_read_splits_follow_runtime_configuration(self):
with tempfile.TemporaryDirectory() as value:
root = Path(value)
pack, manifest, digests = _make_pack(root)
store = ExpertPackStore(
pack,
manifest_path=manifest,
expected_layers=1,
expected_experts=2,
expected_top_k=1,
expected_source_sha256=digests["source"],
expected_model_identity_sha256=digests["model_identity"],
expected_config_sha256=digests["config"],
cache_vram_mib=1,
stage_slots=1,
read_splits=2,
stats_flush_interval=7,
)
ranges = store._object_read_ranges()
self.assertEqual(store.stats["read_splits"], 2)
self.assertEqual(store.stats_flush_interval, 7)
self.assertEqual(len(ranges), 2)
self.assertEqual(ranges[0][0], 0)
self.assertEqual(sum(length for _, length in ranges), 51)
store.close()
def test_short_object_read_raises(self):
with tempfile.TemporaryDirectory() as value:
root = Path(value)
pack, manifest, digests = _make_pack(root)
store = ExpertPackStore(
pack,
manifest_path=manifest,
expected_layers=1,
expected_experts=2,
expected_top_k=1,
expected_source_sha256=digests["source"],
expected_model_identity_sha256=digests["model_identity"],
expected_config_sha256=digests["config"],
cache_vram_mib=1,
stage_slots=1,
)
second_object = store.object_offsets[(0, 1)]
with pack.open("r+b", buffering=0) as stream:
stream.truncate(second_object + 10)
staging = torch.empty(store.object_payload_bytes, dtype=torch.uint8)
with self.assertRaisesRegex(OSError, "short expert-pack read"):
store._read_object(0, 1, staging)
store.close()
def test_duplicate_entry_is_rejected_even_with_valid_pack_hash(self):
with tempfile.TemporaryDirectory() as value:
root = Path(value)
pack, manifest, digests = _make_pack(root)
with pack.open("r+b", buffering=0) as stream:
header = read_header(stream)
entries = read_index(stream, header)
duplicate = replace(
entries[1],
layer=entries[0].layer,
expert=entries[0].expert,
role=entries[0].role,
)
stream.seek(HEADER_STRUCT.size + ENTRY_STRUCT.size)
stream.write(duplicate.pack())
_refresh_manifest_hash(pack, manifest)
with self.assertRaisesRegex(ValueError, "duplicate expert-pack entry"):
ExpertPackStore(
pack,
manifest_path=manifest,
expected_layers=1,
expected_experts=2,
expected_top_k=1,
expected_source_sha256=digests["source"],
expected_model_identity_sha256=digests["model_identity"],
expected_config_sha256=digests["config"],
cache_vram_mib=1,
stage_slots=1,
)
def test_out_of_range_entry_is_rejected_even_with_valid_pack_hash(self):
with tempfile.TemporaryDirectory() as value:
root = Path(value)
pack, manifest, digests = _make_pack(root)
with pack.open("r+b", buffering=0) as stream:
header = read_header(stream)
entries = read_index(stream, header)
invalid = replace(
entries[0], pack_offset=pack.stat().st_size + header.alignment
)
stream.seek(HEADER_STRUCT.size)
stream.write(invalid.pack())
_refresh_manifest_hash(pack, manifest)
with self.assertRaisesRegex(ValueError, "object layout mismatch"):
ExpertPackStore(
pack,
manifest_path=manifest,
expected_layers=1,
expected_experts=2,
expected_top_k=1,
expected_source_sha256=digests["source"],
expected_model_identity_sha256=digests["model_identity"],
expected_config_sha256=digests["config"],
cache_vram_mib=1,
stage_slots=1,
)
def test_source_identity_mismatch_is_rejected(self):
with tempfile.TemporaryDirectory() as value:
root = Path(value)
pack, manifest, digests = _make_pack(root)
with self.assertRaisesRegex(ValueError, "source_blob_sha256"):
ExpertPackStore(
pack,
manifest_path=manifest,
expected_layers=1,
expected_experts=2,
expected_top_k=1,
expected_source_sha256="0" * 64,
expected_model_identity_sha256=digests["model_identity"],
expected_config_sha256=digests["config"],
cache_vram_mib=1,
stage_slots=1,
)
@unittest.skipUnless(torch.cuda.is_available(), "requires CUDA pinned memory")
def test_direct_io_rejects_unaligned_object_ranges(self):
with tempfile.TemporaryDirectory() as value:
root = Path(value)
pack, manifest, digests = _make_pack(root)
store = ExpertPackStore(
pack,
manifest_path=manifest,
expected_layers=1,
expected_experts=2,
expected_top_k=1,
expected_source_sha256=digests["source"],
expected_model_identity_sha256=digests["model_identity"],
expected_config_sha256=digests["config"],
cache_vram_mib=1,
cache_vram_reserve_mib=1,
stage_slots=1,
direct_io=True,
)
with self.assertRaisesRegex(ValueError, "aligned read ranges"):
store.initialize_device_cache("cuda")
store.close()
def test_close_flushes_stats_atomically(self):
with tempfile.TemporaryDirectory() as value:
root = Path(value)
pack, manifest, digests = _make_pack(root)
stats_path = root / "stats.json"
store = ExpertPackStore(
pack,
manifest_path=manifest,
expected_layers=1,
expected_experts=2,
expected_top_k=1,
expected_source_sha256=digests["source"],
expected_model_identity_sha256=digests["model_identity"],
expected_config_sha256=digests["config"],
cache_vram_mib=1,
stage_slots=1,
stats_path=stats_path,
)
store._route_calls_by_layer[0] = 2
store._route_tokens_by_layer[0] = 3
store.stats["pack_reads"] = 4
store.stats["pack_read_bytes"] = 204
store.close()
stats = json.loads(stats_path.read_text(encoding="utf-8"))
self.assertEqual(stats["pack_reads"], 4)
self.assertEqual(stats["pack_read_bytes"], 204)
self.assertEqual(stats["route_calls_by_layer"], [2])
self.assertEqual(stats["route_tokens_by_layer"], [3])
self.assertFalse(list(root.glob("stats.json.*.tmp")))
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,115 @@
# SPDX-License-Identifier: Apache-2.0
import unittest
from types import SimpleNamespace
from unittest.mock import patch
import torch
from sglang.srt.layers.quantization.gguf import (
GGUFLinearMethod,
_ordered_gguf_shard_ids,
)
from sglang.srt.model_loader.kimi_k3_gguf import (
_kda_a_log_target_value,
_residual_target_value,
kimi_k3_checkpoint_targets,
routed_expert_tensor,
)
from sglang.test.ci.ci_register import register_cpu_ci
register_cpu_ci(est_time=10, suite="base-a-test-cpu")
class TestKimiK3GGUFMapping(unittest.TestCase):
def test_maps_dense_kda_mla_moe_and_residual_tensors(self) -> None:
cases = {
"token_embd.weight": ("model.embed_tokens.weight",),
"blk.0.ffn_gate.weight": ("model.layers.0.mlp.gate_proj.weight",),
"blk.1.ssm_g.weight": ("model.layers.1.self_attn.g_proj.weight",),
"blk.3.attn_q_b.weight": ("model.layers.3.self_attn.q_b_proj.weight",),
"blk.3.attn_k_b.weight": ("model.layers.3.self_attn.k_b_qweight",),
"blk.3.attn_v_b.weight": ("model.layers.3.self_attn.v_b_qweight",),
"blk.1.ffn_routed_down.weight": (
"model.layers.1.mlp.routed_expert_down_proj.weight",
),
"blk.1.ffn_gate_shexp.weight": (
"model.layers.1.mlp.shared_experts.gate_proj.weight",
),
"blk.2.attn_res_score.weight": (
"model.layers.2.self_attention_res_proj.weight",
"model.layers.2.self_attention_res_norm.weight",
),
"output_res_score.weight": (
"model.output_attn_res_proj.weight",
"model.output_attn_res_norm.weight",
),
}
for source, expected in cases.items():
with self.subTest(source=source):
self.assertEqual(kimi_k3_checkpoint_targets(source), expected)
def test_only_routed_aggregate_tensors_are_skipped(self) -> None:
self.assertTrue(routed_expert_tensor("blk.92.ffn_up_exps.weight"))
self.assertTrue(routed_expert_tensor("blk.1.ffn_down_exps.weight"))
self.assertFalse(routed_expert_tensor("blk.1.ffn_up_shexp.weight"))
self.assertFalse(routed_expert_tensor("blk.1.ffn_routed_up.weight"))
def test_unknown_tensor_fails_closed(self) -> None:
with self.assertRaisesRegex(KeyError, "unsupported Kimi-K3"):
kimi_k3_checkpoint_targets("blk.7.unexpected.weight")
def test_residual_score_preserves_exact_combined_weight(self) -> None:
source = torch.tensor([0.5, -1.25, 3.0], dtype=torch.float32)
projection = _residual_target_value(source, 0)
norm = _residual_target_value(source, 1)
self.assertEqual(tuple(projection.shape), (1, 3))
self.assertEqual(tuple(norm.shape), (3,))
torch.testing.assert_close(norm * projection.squeeze(0), source)
def test_residual_score_rejects_non_vector_source(self) -> None:
with self.assertRaisesRegex(ValueError, "must be a vector"):
_residual_target_value(torch.ones(1, 3), 0)
def test_restores_kda_a_log_from_gguf_transform(self) -> None:
original = torch.tensor([-0.75, 0.0, 1.5], dtype=torch.float32)
stored = -torch.exp(original)
torch.testing.assert_close(_kda_a_log_target_value(stored), original)
with self.assertRaisesRegex(ValueError, "only -exp"):
_kda_a_log_target_value(torch.tensor([-1.0, 0.0]))
with self.assertRaisesRegex(ValueError, "finite"):
_kda_a_log_target_value(torch.tensor([-1.0, float("nan")]))
def test_merged_gguf_output_uses_logical_shard_order(self) -> None:
qweight = torch.tensor([[30], [0], [20], [10]], dtype=torch.uint8)
qweight.shard_id = [3, 0, 2, 1]
qweight.shard_offset_map = {
3: (0, 1, 1),
0: (1, 2, 1),
2: (2, 3, 1),
1: (3, 4, 1),
}
qweight.gguf_prefix = ""
layer = SimpleNamespace(
qweight=qweight,
qweight_type=SimpleNamespace(shard_weight_type={0: 0, 1: 0, 2: 0, 3: 0}),
)
method = object.__new__(GGUFLinearMethod)
def fake_matmul(_x, weight, _weight_type):
return weight[:, 0].float().unsqueeze(0)
with patch(
"sglang.srt.layers.quantization.gguf.fused_mul_mat_gguf",
side_effect=fake_matmul,
):
output = method.apply(layer, torch.zeros(1, 1))
torch.testing.assert_close(output, torch.tensor([[0.0, 10.0, 20.0, 30.0]]))
def test_unknown_gguf_shard_layouts_preserve_checkpoint_order(self) -> None:
self.assertEqual(_ordered_gguf_shard_ids(["q", "k"]), ["q", "k"])
self.assertEqual(_ordered_gguf_shard_ids([4, 2]), [4, 2])
if __name__ == "__main__":
unittest.main()
@@ -1,7 +1,7 @@
import subprocess
import sys
import unittest
from types import SimpleNamespace
from types import ModuleType, SimpleNamespace
from unittest.mock import MagicMock, Mock, patch
import torch
@@ -279,7 +279,18 @@ class TestMiniCPMSparseMetadata(CustomTestCase):
model_runner.server_args.attention_backend = "minicpm_flashinfer"
flashinfer_adapter = object()
fake_fuse_kernel = ModuleType("sglang.srt.layers.attention.minicpm.fuse_kernel")
fake_fuse_kernel.fused_attn_pooling_online_topk_prefill = Mock(
return_value="prefill"
)
fake_fuse_kernel.fused_attn_pooling_online_topk_decode = Mock(
return_value="decode"
)
with (
patch.dict(
sys.modules,
{"sglang.srt.layers.attention.minicpm.fuse_kernel": fake_fuse_kernel},
),
patch.object(backend_module, "MiniCPMHybridConfig", SimpleNamespace),
patch.object(backend_module, "is_blackwell_supported", return_value=True),
patch.object(
@@ -297,11 +308,6 @@ class TestMiniCPMSparseMetadata(CustomTestCase):
"get_parallel",
return_value=SimpleNamespace(attn_tp_size=1),
),
patch(
"sglang.srt.layers.attention.minicpm.fuse_kernel."
"fused_attn_pooling_online_topk_prefill",
return_value="prefill",
),
patch.object(backend_module, "attach_compressed_cache"),
):
backend = MiniCPMSparseBackend(model_runner, use_flashinfer=True)
@@ -1079,10 +1085,16 @@ class TestMiniCPMSparseMetadata(CustomTestCase):
backend._get_fused_topk_kernel.assert_called_once_with(1, is_prefill=False)
def test_fused_topk_prefill_kernels_compile_for_all_batches_at_startup(self):
with patch(
"sglang.srt.layers.attention.minicpm.fuse_kernel."
"fused_attn_pooling_online_topk_prefill",
side_effect=lambda **kwargs: f"prefill-{kwargs['batch_size']}",
fake_fuse_kernel = ModuleType("sglang.srt.layers.attention.minicpm.fuse_kernel")
fake_fuse_kernel.fused_attn_pooling_online_topk_prefill = Mock(
side_effect=lambda **kwargs: f"prefill-{kwargs['batch_size']}"
)
fake_fuse_kernel.fused_attn_pooling_online_topk_decode = Mock(
side_effect=lambda **kwargs: f"decode-{kwargs['batch_size']}"
)
with patch.dict(
sys.modules,
{"sglang.srt.layers.attention.minicpm.fuse_kernel": fake_fuse_kernel},
):
backend, *_ = _construct_sparse_backend(
max_running_requests=3,
@@ -1104,17 +1116,14 @@ class TestMiniCPMSparseMetadata(CustomTestCase):
backend.fused_kernel_kwargs = {"topk": 8}
backend.prefill_kernel_max_seqlen_q_grid = 64
with (
patch(
"sglang.srt.layers.attention.minicpm.fuse_kernel."
"fused_attn_pooling_online_topk_prefill",
return_value="prefill",
) as prefill,
patch(
"sglang.srt.layers.attention.minicpm.fuse_kernel."
"fused_attn_pooling_online_topk_decode",
return_value="decode",
) as decode,
fake_fuse_kernel = ModuleType("sglang.srt.layers.attention.minicpm.fuse_kernel")
prefill = Mock(return_value="prefill")
decode = Mock(return_value="decode")
fake_fuse_kernel.fused_attn_pooling_online_topk_prefill = prefill
fake_fuse_kernel.fused_attn_pooling_online_topk_decode = decode
with patch.dict(
sys.modules,
{"sglang.srt.layers.attention.minicpm.fuse_kernel": fake_fuse_kernel},
):
self.assertEqual(
backend._get_fused_topk_kernel(3, is_prefill=True), "prefill"
@@ -0,0 +1,24 @@
import unittest
from types import SimpleNamespace
from sglang.srt.lora.deepseek_mla_correction import is_kv_b_lora_active
from sglang.test.ci.ci_register import register_cpu_ci
register_cpu_ci(est_time=5, suite="base-a-test-cpu")
class TestDeepseekMLACorrection(unittest.TestCase):
def test_kv_b_lora_probe(self):
self.assertFalse(is_kv_b_lora_active(SimpleNamespace()))
self.assertFalse(
is_kv_b_lora_active(SimpleNamespace(kv_b_proj=SimpleNamespace()))
)
self.assertTrue(
is_kv_b_lora_active(
SimpleNamespace(kv_b_proj=SimpleNamespace(set_lora=True))
)
)
if __name__ == "__main__":
unittest.main()
+4 -1
View File
@@ -636,7 +636,10 @@ class TestGoldenModelOverrides(_IsolatedPublish):
)
# Thor (SM110) and other architectures keep the existing auto behavior.
with patch.object(overrides_module, "is_sm100_supported", return_value=False):
with (
patch.object(overrides_module, "is_sm100_supported", return_value=False),
patch.object(overrides_module, "is_sm120_supported", return_value=False),
):
non_sm10x = self._construct(
"MiniMaxM2ForCausalLM", "llama", quantization="modelopt_fp4"
)
@@ -139,7 +139,9 @@ _EXPOSED = {
("dllm/config.py", "model_path"),
("multimodal/processors/base_processor.py", "image_processor_backend"),
("speculative/spec_registry.py", "disable_overlap_schedule"),
("disaggregation/encoder/server.py", "model_loader_extra_config"),
("layers/moe/utils.py", "deepep_mode"),
("layers/moe/utils.py", "disable_shared_experts_fusion"),
("layers/moe/utils.py", "moe_a2a_backend"),
("layers/moe/utils.py", "moe_runner_backend"),
("layers/moe/utils.py", "quantization"),
@@ -171,6 +173,8 @@ _EXPOSED = {
("layers/flashinfer_comm_fusion.py", "flashinfer_allreduce_fusion_backend"),
("lora/lora_manager.py", "enable_lora_overlap_loading"),
("lora/marlin_lora_temp/policy.py", "lora_paths"),
("model_loader/expert_pack_runtime.py", "model_path"),
("model_loader/expert_pack_runtime.py", "tokenizer_path"),
("parser/template_detection.py", "model_path"),
("speculative/adaptive_spec_params.py", "speculative_algorithm"),
("speculative/adaptive_spec_params.py", "speculative_eagle_topk"),
@@ -189,6 +193,7 @@ _EXPOSED = {
("weight_cache/daemon.py", "enable_dp_lm_head"),
("weight_cache/daemon.py", "ep_size"),
("weight_cache/daemon.py", "load_format"),
("weight_cache/daemon.py", "model_loader_extra_config"),
("weight_cache/daemon.py", "model_path"),
("weight_cache/daemon.py", "moe_a2a_backend"),
("weight_cache/daemon.py", "moe_dense_tp_size"),
@@ -212,6 +217,7 @@ _OVERRIDDEN_AND_READ = {
("dllm/config.py", "model_path"),
("entrypoints/engine.py", "reasoning_parser"),
("entrypoints/engine.py", "tool_call_parser"),
("model_loader/expert_pack_runtime.py", "model_path"),
("weight_cache/daemon.py", "dp_size"),
("weight_cache/daemon.py", "dtype"),
("weight_cache/daemon.py", "ep_size"),