[Qwen3.8] Enable NVIDIA NVFP4 on DGX Spark with file-backed PLE and PDL router fix (#39126)
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> Co-authored-by: rdxa <rdxa@rdxa-int-spark-01.yvb.moe> Co-authored-by: Yangmin Li <yangminl@nvidia.com> Co-authored-by: Manrique <nanomlm@gmail.com> Co-authored-by: yhyang201 <yhyang201@gmail.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
rdxa
Yangmin Li
Manrique
yhyang201
parent
d6fabb74b4
commit
cebca698e2
@@ -1,3 +1,5 @@
|
||||
import os
|
||||
import tempfile
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
@@ -79,11 +81,11 @@ def _make_source_embedding(
|
||||
)
|
||||
|
||||
|
||||
def _load_rows(offloaded, rows):
|
||||
def _load_rows(offloaded, rows, *, pinned=True):
|
||||
pointer = offloaded.weight.data_ptr()
|
||||
offloaded.weight_loader(offloaded.weight, rows)
|
||||
assert offloaded.weight.data_ptr() == pointer
|
||||
assert offloaded.weight.is_pinned()
|
||||
assert offloaded.weight.is_pinned() == pinned
|
||||
assert offloaded.weight.weight_loader.__self__ is offloaded
|
||||
assert offloaded.quant_method is None
|
||||
|
||||
@@ -189,6 +191,78 @@ def test_qwen4_ple_prefetch_buffer_lifecycle(monkeypatch):
|
||||
assert set(layer._graph_prefetch_buffers) == {3, 5}
|
||||
|
||||
|
||||
def _file_backend_supported() -> bool:
|
||||
from sglang.srt.models.qwen4_exp_ple_table import device_uses_host_page_tables
|
||||
|
||||
return (
|
||||
torch.cuda.is_available()
|
||||
and device_uses_host_page_tables(torch.cuda.current_device()) is True
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not _file_backend_supported(),
|
||||
reason="the file backend needs pageable host memory reachable through host page tables",
|
||||
)
|
||||
@pytest.mark.parametrize("embedding_dim", [7, 160])
|
||||
def test_qwen4_ple_file_backend_matches_pinned(embedding_dim):
|
||||
with tempfile.TemporaryDirectory() as table_dir:
|
||||
pinned = Qwen4ExpPinnedHostEmbedding(
|
||||
_make_source_embedding(embedding_dim=embedding_dim)
|
||||
)
|
||||
filed = Qwen4ExpPinnedHostEmbedding(
|
||||
_make_source_embedding(embedding_dim=embedding_dim),
|
||||
backend="file",
|
||||
table_dir=table_dir,
|
||||
)
|
||||
assert pinned._file_prefetcher is None and filed._file_prefetcher is not None
|
||||
(name,) = os.listdir(table_dir)
|
||||
assert "rows0-8" in name # this rank's vocabulary shard
|
||||
rows = torch.arange(
|
||||
8 * embedding_dim, dtype=torch.bfloat16, device="cuda"
|
||||
).reshape(8, embedding_dim)
|
||||
_load_rows(pinned, rows)
|
||||
_load_rows(filed, rows, pinned=False)
|
||||
ids = torch.tensor([[0, 7, 3], [4, 1, 6]], dtype=torch.int64, device="cuda")
|
||||
torch.testing.assert_close(filed(ids), pinned(ids), rtol=0, atol=0)
|
||||
# A prefill-sized gather goes through the page-cache hint path.
|
||||
big = torch.randint(0, 8, (4096,), device="cuda")
|
||||
torch.testing.assert_close(
|
||||
filed(big), rows.index_select(0, big), rtol=0, atol=0
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not _file_backend_supported(),
|
||||
reason="the file backend needs pageable host memory reachable through host page tables",
|
||||
)
|
||||
def test_qwen4_ple_file_backend_fp8_table():
|
||||
embedding_dim = 160
|
||||
with tempfile.TemporaryDirectory() as table_dir:
|
||||
filed = Qwen4ExpPinnedHostEmbedding(
|
||||
_make_source_embedding(
|
||||
embedding_dim=embedding_dim, dtype=torch.float8_e4m3fn
|
||||
),
|
||||
backend="file",
|
||||
table_dir=table_dir,
|
||||
)
|
||||
assert filed.weight.dtype == torch.float8_e4m3fn
|
||||
rows = (
|
||||
torch.arange(8 * embedding_dim, dtype=torch.float32, device="cuda").reshape(
|
||||
8, embedding_dim
|
||||
)
|
||||
/ 64
|
||||
).to(torch.float8_e4m3fn)
|
||||
_load_rows(filed, rows, pinned=False)
|
||||
ids = torch.tensor([[0, 7, 3]], dtype=torch.int64, device="cuda")
|
||||
expected = (
|
||||
rows.index_select(0, ids.flatten())
|
||||
.to(torch.bfloat16)
|
||||
.reshape(1, 3, embedding_dim)
|
||||
)
|
||||
torch.testing.assert_close(filed(ids), expected, rtol=0, atol=0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
"""Fp8MoEMethod builds a triton runner when the global MoE runner backend is
|
||||
flashinfer_cutlass or flashinfer_cutedsl, which have no fp8 MoE path."""
|
||||
|
||||
from sglang.test.ci.ci_register import register_cpu_ci
|
||||
|
||||
register_cpu_ci(est_time=5, suite="base-a-test-cpu")
|
||||
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
import sglang.srt.layers.quantization.fp8 as fp8
|
||||
from sglang.srt.layers.moe import MoeRunnerBackend, MoeRunnerConfig
|
||||
from sglang.test.test_utils import CustomTestCase
|
||||
|
||||
|
||||
class TestFp8MoeRunnerFallback(CustomTestCase):
|
||||
def _runner_backend_for(self, global_backend):
|
||||
method = fp8.Fp8MoEMethod.__new__(fp8.Fp8MoEMethod)
|
||||
with patch.object(fp8, "get_moe_runner_backend", return_value=global_backend):
|
||||
method.create_moe_runner(layer=None, moe_runner_config=MoeRunnerConfig())
|
||||
return method.runner.runner_backend
|
||||
|
||||
def test_flashinfer_cutlass_falls_back_to_triton(self):
|
||||
self.assertTrue(
|
||||
self._runner_backend_for(MoeRunnerBackend.FLASHINFER_CUTLASS).is_triton()
|
||||
)
|
||||
|
||||
def test_flashinfer_cutedsl_falls_back_to_triton(self):
|
||||
self.assertTrue(
|
||||
self._runner_backend_for(MoeRunnerBackend.FLASHINFER_CUTEDSL).is_triton()
|
||||
)
|
||||
|
||||
def test_triton_is_kept(self):
|
||||
self.assertTrue(self._runner_backend_for(MoeRunnerBackend.TRITON).is_triton())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -22,7 +22,12 @@ from sglang.srt.configs.model_config import ModelConfig
|
||||
from sglang.srt.layers.linear import ReplicatedLinear
|
||||
from sglang.srt.layers.logits_processor import should_apply_lm_head_quant_method
|
||||
from sglang.srt.layers.modelopt_utils import QUANT_CFG_CHOICES
|
||||
from sglang.srt.layers.quantization.fp8 import Fp8Config, Fp8LinearMethod
|
||||
from sglang.srt.layers.moe.fused_moe_triton.layer import FusedMoE
|
||||
from sglang.srt.layers.quantization.fp8 import (
|
||||
Fp8Config,
|
||||
Fp8LinearMethod,
|
||||
Fp8MoEMethod,
|
||||
)
|
||||
from sglang.srt.layers.quantization.modelopt_quant import (
|
||||
ModelOptFp4Config,
|
||||
ModelOptFp4LinearMethod,
|
||||
@@ -1188,6 +1193,56 @@ class TestModelOptMixedPrecisionConfig(CustomTestCase):
|
||||
"FP8",
|
||||
)
|
||||
|
||||
def test_mixed_precision_resolves_vl_language_model_keys(self):
|
||||
# nvidia/Qwen3.8-Flash-Next-NVFP4 keys the text stack as
|
||||
# `model.language_model.*` while Qwen4-Exp modules are `model.*`.
|
||||
quant_config = ModelOptMixedPrecisionConfig.from_config(
|
||||
{
|
||||
"quant_algo": "MIXED_PRECISION",
|
||||
"quantized_layers": {
|
||||
"model.language_model.layers.3.mlp.experts": {
|
||||
"quant_algo": "NVFP4",
|
||||
"group_size": 16,
|
||||
},
|
||||
"model.language_model.layers.1.ple.ple_embedding.ngram_embedding": {
|
||||
"quant_algo": "FP8"
|
||||
},
|
||||
"mtp.layers.0.mlp.experts": {
|
||||
"quant_algo": "FP8_BLOCK_SCALES",
|
||||
"group_size": 128,
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
self.assertEqual(quant_config.exclude_modules, [])
|
||||
moe = FusedMoE.__new__(FusedMoE)
|
||||
self.assertIsInstance(
|
||||
quant_config.get_quant_method(moe, "mtp.layers.0.mlp.experts"),
|
||||
Fp8MoEMethod,
|
||||
)
|
||||
self.assertEqual(
|
||||
quant_config.get_quant_method(
|
||||
moe, "mtp.layers.0.mlp.experts"
|
||||
).quant_config.weight_block_size,
|
||||
[128, 128],
|
||||
)
|
||||
self.assertEqual(
|
||||
quant_config.resolve_quant_algo("model.layers.3.mlp.experts"), "NVFP4"
|
||||
)
|
||||
self.assertEqual(
|
||||
quant_config.resolve_quant_algo(
|
||||
"model.layers.1.ple.ple_embedding.ngram_embedding"
|
||||
),
|
||||
"FP8",
|
||||
)
|
||||
self.assertIsNone(
|
||||
quant_config.resolve_quant_algo("model.layers.1.ple.key_proj")
|
||||
)
|
||||
self.assertIsNone(
|
||||
quant_config.resolve_quant_algo("model.layers.3.mlp.shared_expert")
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -0,0 +1,332 @@
|
||||
"""File-backed host storage for the offloaded Qwen4-Exp PLE table.
|
||||
|
||||
CPU part: the allocator builds a sparse file of exactly the table's size, hands
|
||||
back a tensor with the requested shape/dtype whose writes land in the file and
|
||||
survive a re-open, reuses the file across calls, replaces one of the wrong size,
|
||||
and the prefetcher computes the right page set and honours its size floor. The
|
||||
resident-set trimmer measures only its own mapping, drops its pages once over
|
||||
budget without losing what was written through them, and is off when the budget
|
||||
is zero or the mapping is pinned.
|
||||
|
||||
GPU part (skipped unless the device reads pageable host memory through the host
|
||||
page tables, i.e. unified-memory parts such as GB10): the production Triton
|
||||
gather kernel reading from the file-backed table matches a torch gather.
|
||||
"""
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.srt.models.qwen4_exp_ple_table import (
|
||||
PleFilePrefetcher,
|
||||
PleFileRssTrimmer,
|
||||
_mapping_rss_bytes,
|
||||
allocate_ple_host_table,
|
||||
default_ple_table_dir,
|
||||
device_uses_host_page_tables,
|
||||
make_ple_file_prefetcher,
|
||||
make_ple_file_rss_trimmer,
|
||||
ple_table_file_name,
|
||||
)
|
||||
from sglang.test.ci.ci_register import register_cpu_ci
|
||||
from sglang.test.test_utils import CustomTestCase
|
||||
|
||||
register_cpu_ci(est_time=10, suite="base-a-test-cpu")
|
||||
|
||||
|
||||
class TestPleFileTableAllocator(CustomTestCase):
|
||||
def test_file_is_sparse_and_sized_exactly(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
table = allocate_ple_host_table((1000, 160), torch.float8_e4m3fn, "file", d)
|
||||
path = os.path.join(
|
||||
d, ple_table_file_name((1000, 160), torch.float8_e4m3fn)
|
||||
)
|
||||
self.assertTrue(os.path.exists(path))
|
||||
self.assertEqual(os.path.getsize(path), 1000 * 160)
|
||||
self.assertEqual(tuple(table.shape), (1000, 160))
|
||||
self.assertEqual(table.dtype, torch.float8_e4m3fn)
|
||||
# Sparse: nothing written yet, so (almost) no blocks allocated.
|
||||
self.assertLess(os.stat(path).st_blocks * 512, 64 * 1024)
|
||||
|
||||
def test_writes_persist_and_file_is_reused(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
shape, dtype = (64, 32), torch.bfloat16
|
||||
table = allocate_ple_host_table(shape, dtype, "file", d)
|
||||
row = torch.arange(32, dtype=torch.float32).to(dtype)
|
||||
table[7].copy_(row) # what the weight loader does, row by row
|
||||
del table
|
||||
again = allocate_ple_host_table(shape, dtype, "file", d)
|
||||
self.assertTrue(torch.equal(again[7].float(), row.float()))
|
||||
self.assertEqual(len(os.listdir(d)), 1)
|
||||
|
||||
def test_wrong_sized_file_is_replaced(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
path = os.path.join(d, ple_table_file_name((8, 8), torch.bfloat16))
|
||||
with open(path, "wb") as f:
|
||||
f.write(b"\x01" * 10)
|
||||
table = allocate_ple_host_table((8, 8), torch.bfloat16, "file", d)
|
||||
self.assertEqual(os.path.getsize(path), 8 * 8 * 2)
|
||||
self.assertEqual(tuple(table.shape), (8, 8))
|
||||
|
||||
def test_tag_separates_tensor_parallel_shards(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
a = allocate_ple_host_table(
|
||||
(8, 8), torch.bfloat16, "file", d, tag="rows0-8"
|
||||
)
|
||||
b = allocate_ple_host_table(
|
||||
(8, 8), torch.bfloat16, "file", d, tag="rows8-16"
|
||||
)
|
||||
a.fill_(1.0)
|
||||
self.assertEqual(len(os.listdir(d)), 2)
|
||||
self.assertTrue(torch.all(b.float() == 0.0))
|
||||
self.assertIn(
|
||||
"rows8-16", ple_table_file_name((8, 8), torch.bfloat16, "rows8-16")
|
||||
)
|
||||
|
||||
def test_default_dir_is_per_checkpoint(self):
|
||||
with mock.patch.dict(os.environ, {"SGLANG_QWEN4_PLE_FILE_DIR": "/cache/ple"}):
|
||||
a = default_ple_table_dir("RadixArk/Qwen3.8-Flash-Next-NVFP4")
|
||||
b = default_ple_table_dir("/root/.cache/huggingface/flashnext-fp8/")
|
||||
self.assertEqual(a, "/cache/ple/RadixArk_Qwen3.8-Flash-Next-NVFP4")
|
||||
self.assertEqual(b, "/cache/ple/root_.cache_huggingface_flashnext-fp8")
|
||||
self.assertNotEqual(a, b)
|
||||
|
||||
def test_unknown_backend_rejected(self):
|
||||
with self.assertRaises(ValueError):
|
||||
allocate_ple_host_table((4, 4), torch.bfloat16, "nvme", None)
|
||||
|
||||
def test_pinned_backend_unchanged(self):
|
||||
if not torch.cuda.is_available():
|
||||
self.skipTest("pinned memory needs a CUDA runtime")
|
||||
table = allocate_ple_host_table((4, 4), torch.bfloat16, "pinned", None)
|
||||
self.assertTrue(table.is_pinned())
|
||||
self.assertIsNone(make_ple_file_prefetcher(table))
|
||||
|
||||
|
||||
class TestPleFilePrefetcher(CustomTestCase):
|
||||
def test_page_set_covers_row_start_and_end(self):
|
||||
# 160-byte rows: row 25 spans bytes 4000-4159, i.e. pages 0 and 1.
|
||||
pages = PleFilePrefetcher.pages_for_rows(torch.tensor([25, 0]), 160)
|
||||
self.assertEqual(pages, [0, 1])
|
||||
pages = PleFilePrefetcher.pages_for_rows(torch.tensor([1000, 1000]), 160)
|
||||
self.assertEqual(pages, [39]) # dedup, single page
|
||||
|
||||
def test_enqueue_uses_local_tp_offsets_and_ignores_other_shards(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
path = os.path.join(d, "t.bin")
|
||||
with open(path, "wb") as f:
|
||||
f.truncate(8192)
|
||||
pf = PleFilePrefetcher(path, row_bytes=160, min_rows=1)
|
||||
try:
|
||||
with mock.patch("os.posix_fadvise") as fadvise:
|
||||
self.assertFalse(
|
||||
pf.enqueue(
|
||||
torch.tensor([999, 1032]), vocab_start=1000, vocab_end=1032
|
||||
)
|
||||
)
|
||||
self.assertTrue(
|
||||
pf.enqueue(
|
||||
torch.tensor([999, 1000, 1025, 1032]),
|
||||
vocab_start=1000,
|
||||
vocab_end=1032,
|
||||
)
|
||||
)
|
||||
pf._pool.shutdown(wait=True)
|
||||
self.assertEqual(
|
||||
sorted(c.args[1] for c in fadvise.call_args_list), [0, 4096]
|
||||
)
|
||||
finally:
|
||||
pf.close()
|
||||
|
||||
def test_enqueue_respects_min_rows_and_advises_pages(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
path = os.path.join(d, "t.bin")
|
||||
with open(path, "wb") as f:
|
||||
f.truncate(1 << 20)
|
||||
pf = PleFilePrefetcher(path, row_bytes=160, min_rows=4)
|
||||
try:
|
||||
self.assertFalse(pf.enqueue(torch.tensor([1, 2, 3])))
|
||||
with mock.patch("os.posix_fadvise") as fadvise:
|
||||
self.assertTrue(pf.enqueue(torch.tensor([0, 1, 2, 30])))
|
||||
pf._pool.shutdown(wait=True)
|
||||
offsets = sorted(c.args[1] for c in fadvise.call_args_list)
|
||||
# rows 0-2 live in page 0; row 30 (bytes 4800-4959) in page 1
|
||||
self.assertEqual(offsets, [0, 4096])
|
||||
finally:
|
||||
pf.close()
|
||||
|
||||
|
||||
class TestSmapsParsing(CustomTestCase):
|
||||
"""The parser runs anywhere; only the live mapping needs Linux."""
|
||||
|
||||
SMAPS = """00400000-00401000 r--p 00000000 08:01 1 /usr/bin/x
|
||||
Size: 4 kB
|
||||
Rss: 4 kB
|
||||
7f0000000000-7f0004000000 rw-s 00000000 08:01 2 /cache/ple/table.bin
|
||||
Size: 65536 kB
|
||||
Rss: 32768 kB
|
||||
7f0004000000-7f0008000000 rw-s 00000000 08:01 2 /cache/ple/table.bin
|
||||
Size: 65536 kB
|
||||
Rss: 1024 kB
|
||||
7f0100000000-7f0100001000 rw-p 00000000 00:00 0
|
||||
Size: 4 kB
|
||||
Rss: 4 kB
|
||||
"""
|
||||
|
||||
def _rss(self, addr, nbytes):
|
||||
with tempfile.NamedTemporaryFile("w", suffix=".smaps", delete=False) as f:
|
||||
f.write(self.SMAPS)
|
||||
path = f.name
|
||||
try:
|
||||
return _mapping_rss_bytes(addr, nbytes, smaps_path=path)
|
||||
finally:
|
||||
os.unlink(path)
|
||||
|
||||
def test_sums_every_vma_of_the_table_and_nothing_else(self):
|
||||
# The table spans both of its VMAs; the unrelated ones must not count.
|
||||
self.assertEqual(self._rss(0x7F0000000000, 0x8000000), (32768 + 1024) * 1024)
|
||||
|
||||
def test_counts_a_partially_overlapping_vma(self):
|
||||
# A range ending inside the first VMA still needs that VMA's pages.
|
||||
self.assertEqual(self._rss(0x7F0000000000, 0x1000), 32768 * 1024)
|
||||
|
||||
def test_ignores_unrelated_mappings(self):
|
||||
self.assertEqual(self._rss(0x7F0200000000, 0x1000), 0)
|
||||
|
||||
def test_missing_smaps_is_reported_as_unknown(self):
|
||||
self.assertIsNone(
|
||||
_mapping_rss_bytes(0x1000, 0x1000, smaps_path="/nonexistent/smaps")
|
||||
)
|
||||
|
||||
|
||||
class TestPleFileRssTrimmerConfig(CustomTestCase):
|
||||
def test_budget_zero_disables_the_trimmer(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
table = allocate_ple_host_table((64, 32), torch.bfloat16, "file", d)
|
||||
with mock.patch.dict(
|
||||
os.environ, {"SGLANG_QWEN4_PLE_FILE_RSS_BUDGET_GB": "0"}
|
||||
):
|
||||
self.assertIsNone(make_ple_file_rss_trimmer(table))
|
||||
|
||||
def test_pinned_table_has_no_trimmer(self):
|
||||
if not torch.cuda.is_available():
|
||||
self.skipTest("pinned memory needs a CUDA runtime")
|
||||
table = allocate_ple_host_table((4, 4), torch.bfloat16, "pinned", None)
|
||||
self.assertIsNone(make_ple_file_rss_trimmer(table))
|
||||
|
||||
|
||||
@unittest.skipUnless(
|
||||
os.path.exists("/proc/self/smaps"),
|
||||
"the resident set of a mapping is only readable on Linux",
|
||||
)
|
||||
class TestPleFileRssTrimmer(CustomTestCase):
|
||||
# 64 MiB: large enough that the Rss of the mapping stands out, small
|
||||
# enough to write in a CPU test.
|
||||
SHAPE = (32768, 1024)
|
||||
NBYTES = 32768 * 1024 * 2
|
||||
|
||||
def _trimmer(self, table, budget_bytes):
|
||||
return PleFileRssTrimmer(
|
||||
addr=table.data_ptr(),
|
||||
nbytes=self.NBYTES,
|
||||
budget_bytes=budget_bytes,
|
||||
interval_s=3600.0,
|
||||
chunk_bytes=16 << 20, # several chunks, as in production
|
||||
)
|
||||
|
||||
def test_measures_its_own_mapping_only(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
table = allocate_ple_host_table(self.SHAPE, torch.bfloat16, "file", d)
|
||||
trimmer = self._trimmer(table, 0)
|
||||
empty = trimmer.mapping_rss_bytes()
|
||||
table.fill_(1.0) # touches every page
|
||||
touched = trimmer.mapping_rss_bytes()
|
||||
self.assertIsNotNone(touched)
|
||||
self.assertGreater(touched, empty)
|
||||
self.assertGreater(touched, self.NBYTES // 2)
|
||||
# Never the whole process: only the VMAs backing this table.
|
||||
self.assertLessEqual(touched, self.NBYTES + (16 << 20))
|
||||
|
||||
def test_over_budget_drops_pages_and_keeps_the_data(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
table = allocate_ple_host_table(self.SHAPE, torch.bfloat16, "file", d)
|
||||
table.fill_(1.0)
|
||||
table[7][3] = 2.0
|
||||
trimmer = self._trimmer(table, budget_bytes=1 << 20)
|
||||
before = trimmer.mapping_rss_bytes()
|
||||
freed = trimmer.trim_once()
|
||||
after = trimmer.mapping_rss_bytes()
|
||||
self.assertGreater(freed, 0)
|
||||
self.assertLess(after, before // 2)
|
||||
# MADV_DONTNEED on a shared file mapping drops the page-table
|
||||
# entries, not the page cache: the writes are still there.
|
||||
self.assertEqual(table[7][3].item(), 2.0)
|
||||
self.assertEqual(table[9][3].item(), 1.0)
|
||||
|
||||
def test_under_budget_is_a_no_op(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
table = allocate_ple_host_table(self.SHAPE, torch.bfloat16, "file", d)
|
||||
table.fill_(1.0)
|
||||
trimmer = self._trimmer(table, budget_bytes=self.NBYTES * 4)
|
||||
before = trimmer.mapping_rss_bytes()
|
||||
self.assertEqual(trimmer.trim_once(), 0)
|
||||
self.assertEqual(trimmer.mapping_rss_bytes(), before)
|
||||
|
||||
def test_factory_starts_and_stops_a_thread(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
table = allocate_ple_host_table((64, 32), torch.bfloat16, "file", d)
|
||||
with mock.patch.dict(
|
||||
os.environ,
|
||||
{
|
||||
"SGLANG_QWEN4_PLE_FILE_RSS_BUDGET_GB": "1",
|
||||
"SGLANG_QWEN4_PLE_FILE_RSS_INTERVAL_S": "3600",
|
||||
},
|
||||
):
|
||||
trimmer = make_ple_file_rss_trimmer(table)
|
||||
self.assertIsNotNone(trimmer)
|
||||
try:
|
||||
self.assertTrue(trimmer._thread.is_alive())
|
||||
finally:
|
||||
trimmer.close()
|
||||
trimmer._thread.join(timeout=5)
|
||||
self.assertFalse(trimmer._thread.is_alive())
|
||||
|
||||
|
||||
@unittest.skipUnless(
|
||||
torch.cuda.is_available() and device_uses_host_page_tables(0) is True,
|
||||
"needs a device that reads pageable host memory through the host page tables",
|
||||
)
|
||||
class TestPleFileTableGatherOnDevice(CustomTestCase):
|
||||
def test_triton_gather_reads_file_backed_table(self):
|
||||
import triton
|
||||
|
||||
from sglang.srt.models.qwen4_exp import (
|
||||
_gather_ple_embedding_from_pinned_kernel,
|
||||
)
|
||||
|
||||
rows, dim = 4096, 160
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
table = allocate_ple_host_table((rows, dim), torch.bfloat16, "file", d)
|
||||
table.copy_(torch.randn(rows, dim).to(torch.bfloat16))
|
||||
ids = torch.randint(0, rows, (2048,), device="cuda")
|
||||
out = torch.empty(2048, dim, dtype=torch.bfloat16, device="cuda")
|
||||
_gather_ple_embedding_from_pinned_kernel[(ids.numel(),)](
|
||||
table.data_ptr(),
|
||||
ids,
|
||||
out,
|
||||
embedding_dim=dim,
|
||||
tp_vocab_start=0,
|
||||
tp_vocab_end=rows,
|
||||
is_fp8=False,
|
||||
BLOCK_D=triton.next_power_of_2(dim),
|
||||
)
|
||||
torch.cuda.synchronize()
|
||||
expected = table[ids.cpu()].to("cuda")
|
||||
self.assertTrue(torch.equal(out, expected))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -656,7 +656,20 @@ class TestWrapperEntryClassGates(_FusionGateCase):
|
||||
)
|
||||
|
||||
# The normalization the constructor applies, shared with the gate.
|
||||
self.assertIsNone(_mtp_quant_config(_quant("modelopt_mixed")))
|
||||
mixed_bf16_mtp = SimpleNamespace(
|
||||
get_name=lambda: "modelopt_mixed",
|
||||
quantized_layers={"model.layers.0.mlp.experts": {"quant_algo": "NVFP4"}},
|
||||
)
|
||||
self.assertIsNone(_mtp_quant_config(mixed_bf16_mtp))
|
||||
# MIXED_PRECISION checkpoints that quantize the MTP head keep it.
|
||||
mixed_fp8_mtp = SimpleNamespace(
|
||||
get_name=lambda: "modelopt_mixed",
|
||||
quantized_layers={
|
||||
"model.layers.0.mlp.experts": {"quant_algo": "NVFP4"},
|
||||
"mtp.layers.0.mlp.experts": {"quant_algo": "FP8_BLOCK_SCALES"},
|
||||
},
|
||||
)
|
||||
self.assertIs(_mtp_quant_config(mixed_fp8_mtp), mixed_fp8_mtp)
|
||||
serialized = SimpleNamespace(
|
||||
get_name=lambda: "modelopt_fp4", is_checkpoint_nvfp4_serialized=True
|
||||
)
|
||||
|
||||
@@ -637,6 +637,22 @@ class TestGoldenModelOverrides(_IsolatedPublish):
|
||||
pp_size=2,
|
||||
)
|
||||
|
||||
def test_qwen4_ple_file_requires_offload(self):
|
||||
qwen4 = ("Qwen4ExpForConditionalGeneration", "qwen4_exp")
|
||||
with override_platform(is_cuda=True):
|
||||
sa = self._construct(
|
||||
*qwen4,
|
||||
ple_offload_embedding=True,
|
||||
ple_offload_backend="file",
|
||||
ple_offload_dir="/tmp/ple",
|
||||
)
|
||||
self.assertEqual(self._resolved(sa, "ple_offload_backend"), "file")
|
||||
self.assertEqual(self._resolved(sa, "ple_offload_dir"), "/tmp/ple")
|
||||
with self.assertRaisesRegex(ValueError, "requires --ple-offload-embedding"):
|
||||
self._construct(
|
||||
*qwen4, ple_offload_embedding=False, ple_offload_backend="file"
|
||||
)
|
||||
|
||||
def test_qwen4_ple_offload_default(self):
|
||||
qwen4 = ("Qwen4ExpForConditionalGeneration", "qwen4_exp")
|
||||
with override_platform(is_cuda=True):
|
||||
@@ -3072,6 +3088,93 @@ class TestGoldenModelOverrides(_IsolatedPublish):
|
||||
with override_platform(is_sm100=False):
|
||||
self.assertEqual(_qwen3_moe_family_overrides(None, None), {})
|
||||
|
||||
def test_qwen3_moe_family_mixed_precision_moe_runner(self):
|
||||
from sglang.srt.arg_groups.model_overrides.qwen3_moe import (
|
||||
_qwen3_moe_family_overrides,
|
||||
)
|
||||
|
||||
def _mixed(expert_algo):
|
||||
return SimpleNamespace(
|
||||
architectures=["Qwen4ExpForConditionalGeneration"],
|
||||
quantization_config={
|
||||
"quant_method": "modelopt_mixed",
|
||||
"quantized_layers": {
|
||||
"model.language_model.layers.0.mlp.experts": {
|
||||
"quant_algo": expert_algo
|
||||
}
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
args = SimpleNamespace(
|
||||
quantization="modelopt_mixed",
|
||||
_quantization_explicitly_unset=False,
|
||||
moe_a2a_backend="none",
|
||||
moe_runner_backend="auto",
|
||||
)
|
||||
with override_platform(is_sm100=True):
|
||||
# W4A4 experts take trtllm-gen like modelopt_fp4; W4A16 has no
|
||||
# trtllm-gen kernel and goes to marlin.
|
||||
self.assertEqual(
|
||||
_qwen3_moe_family_overrides(args, _mixed("NVFP4")),
|
||||
{"moe_runner_backend": "flashinfer_trtllm"},
|
||||
)
|
||||
self.assertEqual(
|
||||
_qwen3_moe_family_overrides(args, _mixed("W4A16_NVFP4")),
|
||||
{"moe_runner_backend": "marlin"},
|
||||
)
|
||||
|
||||
def test_qwen3_moe_family_w4a16_explicit_runner(self):
|
||||
"""Keep opted-in CuTe DSL v2 W4A16 accepted and auto routed to Marlin."""
|
||||
from sglang.srt.arg_groups.model_overrides.qwen3_moe import (
|
||||
_qwen3_moe_family_overrides,
|
||||
)
|
||||
|
||||
hf_config = SimpleNamespace(
|
||||
architectures=["Qwen4ExpForConditionalGeneration"],
|
||||
quantization_config={
|
||||
"quant_method": "modelopt_mixed",
|
||||
"quantized_layers": {
|
||||
"model.language_model.layers.0.mlp.experts": {
|
||||
"quant_algo": "W4A16_NVFP4"
|
||||
}
|
||||
},
|
||||
},
|
||||
)
|
||||
cases = [
|
||||
("auto", "none", False, {"moe_runner_backend": "marlin"}),
|
||||
("auto", "none", True, {"moe_runner_backend": "marlin"}),
|
||||
("marlin", "none", False, {}),
|
||||
("marlin", "none", True, {}),
|
||||
("flashinfer_cutedsl", "none", True, {}),
|
||||
("flashinfer_cutedsl", "flashinfer", True, {}),
|
||||
("flashinfer_cutedsl", "none", False, None),
|
||||
("flashinfer_cutedsl", "flashinfer", False, None),
|
||||
("flashinfer_cutedsl", "deepep", True, None),
|
||||
("flashinfer_cutlass", "none", True, None),
|
||||
("flashinfer_trtllm", "none", True, None),
|
||||
]
|
||||
for runner, a2a, w4a16_enabled, expected in cases:
|
||||
with (
|
||||
self.subTest(runner=runner, a2a=a2a, w4a16=w4a16_enabled),
|
||||
override_platform(is_sm100=True),
|
||||
envs.SGLANG_FLASHINFER_CUTEDSL_NVFP4_W4A16.override(w4a16_enabled),
|
||||
):
|
||||
args = SimpleNamespace(
|
||||
quantization=None,
|
||||
_quantization_explicitly_unset=False,
|
||||
moe_a2a_backend=a2a,
|
||||
moe_runner_backend=runner,
|
||||
)
|
||||
if expected is None:
|
||||
with self.assertRaisesRegex(ValueError, "W4A16_NVFP4"):
|
||||
_qwen3_moe_family_overrides(args, hf_config)
|
||||
else:
|
||||
self.assertEqual(
|
||||
_qwen3_moe_family_overrides(args, hf_config),
|
||||
{"quantization": "modelopt_mixed", **expected},
|
||||
)
|
||||
|
||||
def test_step3p_declarations_at_callable_level(self):
|
||||
from sglang.srt.arg_groups.overrides import _step3p_overrides
|
||||
|
||||
|
||||
Reference in New Issue
Block a user