[Qwen4-Exp] Build the offloaded PLE table on the meta device so --ple-offload-embedding never materialises it on the accelerator (#39928)
Co-authored-by: Yangmin Li <yangminl@nvidia.com>
This commit is contained in:
co-authored by
Yangmin Li
parent
5f017ffabb
commit
e97614d10c
@@ -508,7 +508,11 @@ class Qwen4ExpNGramEmbedding(nn.Module):
|
|||||||
and not self.use_attn_tp_ngram
|
and not self.use_attn_tp_ngram
|
||||||
)
|
)
|
||||||
ngram_prefix = f"{prefix}.ngram_embedding" if prefix else "ngram_embedding"
|
ngram_prefix = f"{prefix}.ngram_embedding" if prefix else "ngram_embedding"
|
||||||
self.ngram_embedding = VocabParallelEmbedding(
|
offload_embedding = bool(config.ple_offload_embedding)
|
||||||
|
# Offload only needs this embedding's metadata: build it on meta so the
|
||||||
|
# shard is never allocated on the device.
|
||||||
|
with torch.device("meta") if offload_embedding else nullcontext():
|
||||||
|
ngram_embedding = VocabParallelEmbedding(
|
||||||
padded_vocab_size,
|
padded_vocab_size,
|
||||||
self.head_dim_per_ngram,
|
self.head_dim_per_ngram,
|
||||||
params_dtype=(
|
params_dtype=(
|
||||||
@@ -519,9 +523,17 @@ class Qwen4ExpNGramEmbedding(nn.Module):
|
|||||||
output_dtype=torch.bfloat16,
|
output_dtype=torch.bfloat16,
|
||||||
use_attn_tp_group=self.use_attn_tp_ngram,
|
use_attn_tp_group=self.use_attn_tp_ngram,
|
||||||
)
|
)
|
||||||
self.ngram_embedding.register_buffer(
|
# weight_scale stays a real device tensor.
|
||||||
|
ngram_embedding.register_buffer(
|
||||||
"weight_scale", torch.ones(1, dtype=torch.bfloat16), persistent=True
|
"weight_scale", torch.ones(1, dtype=torch.bfloat16), persistent=True
|
||||||
)
|
)
|
||||||
|
if offload_embedding:
|
||||||
|
ngram_embedding = Qwen4ExpPinnedHostEmbedding(
|
||||||
|
ngram_embedding,
|
||||||
|
backend=getattr(config, "ple_offload_backend", "pinned"),
|
||||||
|
table_dir=getattr(config, "ple_offload_dir", None),
|
||||||
|
)
|
||||||
|
self.ngram_embedding = ngram_embedding
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _splitmix64(cls, x: int) -> int:
|
def _splitmix64(cls, x: int) -> int:
|
||||||
@@ -771,6 +783,8 @@ class Qwen4ExpPinnedHostEmbedding(VocabParallelEmbedding):
|
|||||||
|
|
||||||
The table stays in its checkpoint storage dtype (fp8 with a per-tensor
|
The table stays in its checkpoint storage dtype (fp8 with a per-tensor
|
||||||
weight_scale for fp8 checkpoints, bf16 otherwise); gathers emit bf16.
|
weight_scale for fp8 checkpoints, bf16 otherwise); gathers emit bf16.
|
||||||
|
|
||||||
|
The source weight may be on the meta device; only its metadata is used.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_COPIED_ATTRIBUTES = (
|
_COPIED_ATTRIBUTES = (
|
||||||
@@ -931,12 +945,6 @@ class Qwen4ExpPLELayer(nn.Module):
|
|||||||
quant_config=quant_config,
|
quant_config=quant_config,
|
||||||
prefix=f"{prefix}.ple_embedding" if prefix else "ple_embedding",
|
prefix=f"{prefix}.ple_embedding" if prefix else "ple_embedding",
|
||||||
)
|
)
|
||||||
if config.ple_offload_embedding:
|
|
||||||
self.ple_embedding.ngram_embedding = Qwen4ExpPinnedHostEmbedding(
|
|
||||||
self.ple_embedding.ngram_embedding,
|
|
||||||
backend=getattr(config, "ple_offload_backend", "pinned"),
|
|
||||||
table_dir=getattr(config, "ple_offload_dir", None),
|
|
||||||
)
|
|
||||||
self.short_conv_dilation = self.ple_embedding.ngram_size
|
self.short_conv_dilation = self.ple_embedding.ngram_size
|
||||||
self.short_conv_state_len = (
|
self.short_conv_state_len = (
|
||||||
self.conv_kernel_size - 1
|
self.conv_kernel_size - 1
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import pytest
|
|||||||
import torch
|
import torch
|
||||||
from torch import nn
|
from torch import nn
|
||||||
|
|
||||||
|
from sglang.srt.configs.qwen4_exp import Qwen4ExpTextConfig
|
||||||
from sglang.srt.layers.quantization.unquant import UnquantizedEmbeddingMethod
|
from sglang.srt.layers.quantization.unquant import UnquantizedEmbeddingMethod
|
||||||
from sglang.srt.layers.vocab_parallel_embedding import (
|
from sglang.srt.layers.vocab_parallel_embedding import (
|
||||||
VocabParallelEmbeddingShardIndices,
|
VocabParallelEmbeddingShardIndices,
|
||||||
@@ -15,6 +16,7 @@ from sglang.srt.models.qwen4_exp import (
|
|||||||
Qwen4ExpPinnedHostEmbedding,
|
Qwen4ExpPinnedHostEmbedding,
|
||||||
Qwen4ExpPLELayer,
|
Qwen4ExpPLELayer,
|
||||||
)
|
)
|
||||||
|
from sglang.srt.runtime_context import get_context, get_parallel
|
||||||
from sglang.srt.utils import set_weight_attrs
|
from sglang.srt.utils import set_weight_attrs
|
||||||
from sglang.test.ci.ci_register import register_cuda_ci
|
from sglang.test.ci.ci_register import register_cuda_ci
|
||||||
|
|
||||||
@@ -191,6 +193,52 @@ def test_qwen4_ple_prefetch_buffer_lifecycle(monkeypatch):
|
|||||||
assert set(layer._graph_prefetch_buffers) == {3, 5}
|
assert set(layer._graph_prefetch_buffers) == {3, 5}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def single_rank_runtime_context():
|
||||||
|
"""``Qwen4ExpPLELayer.__init__`` reads the TP topology through
|
||||||
|
``VocabParallelEmbedding``; pin it to one rank without a process group."""
|
||||||
|
override = get_context().override_server_args(tp_size=1)
|
||||||
|
override.install()
|
||||||
|
try:
|
||||||
|
with get_parallel().override(
|
||||||
|
tp_rank=0, tp_size=1, attn_tp_rank=0, attn_tp_size=1
|
||||||
|
):
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
override.restore()
|
||||||
|
|
||||||
|
|
||||||
|
def test_qwen4_ple_offload_avoids_device_table(single_rank_runtime_context):
|
||||||
|
# sgl-project/sglang#39841: the table was built on the device before the
|
||||||
|
# host table existed, so the flag needed a full per-rank shard of free VRAM.
|
||||||
|
# Small everywhere except the n-gram table (16 heads x ~20k rows x 4 dims),
|
||||||
|
# which must dominate the layer's footprint for the peak check to bite.
|
||||||
|
config = Qwen4ExpTextConfig(
|
||||||
|
vocab_size=64,
|
||||||
|
hidden_size=16,
|
||||||
|
hc_count=2,
|
||||||
|
ple_embed_dim=64,
|
||||||
|
ngram_size=3,
|
||||||
|
heads_per_ngram=8,
|
||||||
|
ngram_vocab_size_base=20_000,
|
||||||
|
eos_token_id=1,
|
||||||
|
ple_offload_embedding=True,
|
||||||
|
)
|
||||||
|
torch.cuda.synchronize()
|
||||||
|
torch.cuda.reset_peak_memory_stats()
|
||||||
|
base = torch.cuda.memory_allocated()
|
||||||
|
with torch.device("cuda"): # the model loader builds every layer this way
|
||||||
|
layer = Qwen4ExpPLELayer(config, prefix="ple", layer_id=0, ple_layer_index=0)
|
||||||
|
peak = torch.cuda.max_memory_allocated() - base
|
||||||
|
|
||||||
|
emb = layer.ple_embedding.ngram_embedding
|
||||||
|
table_bytes = emb.weight.numel() * emb.weight.element_size()
|
||||||
|
assert peak < table_bytes // 2, (peak, table_bytes)
|
||||||
|
assert emb.weight.device.type == "cpu" and emb.weight.is_pinned()
|
||||||
|
assert emb.weight_scale.is_cuda
|
||||||
|
assert not any(t.is_meta for t in (*layer.parameters(), *layer.buffers()))
|
||||||
|
|
||||||
|
|
||||||
def _file_backend_supported() -> bool:
|
def _file_backend_supported() -> bool:
|
||||||
from sglang.srt.models.qwen4_exp_ple_table import device_uses_host_page_tables
|
from sglang.srt.models.qwen4_exp_ple_table import device_uses_host_page_tables
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user