[Qwen4Exp][ROCm] PLE n-gram table CPU offload (#57497)
Signed-off-by: Mathew Odden <modden@redhat.com> Signed-off-by: Andreas Karatzas <akaratza@amd.com> Co-authored-by: opencode+deepseek-v4-flash+vllm <opencode+deepseek-v4-flash+vllm@example.com> Co-authored-by: Andreas Karatzas <akaratza@amd.com> Co-authored-by: OpenAI Codex <noreply@openai.com>
Mathew Odden committed
Sep 24, 2026 at 23:28 UTC
267eee54bf232e4008a7e0b510f19a62e23aaaca
7 files changed
+887
-529
.buildkite/test_areas/models_basic.yaml
+2
@@ -169,6 +169,8 @@ steps:
169
- image-build-amd
170
commands:
171
- pytest -v -s models/qwen4_exp/test_qsa_amd.py
172
+ # PLE and QSA import conflicting HC registrations in a shared process.
173
+ - pytest -v -s models/qwen4_exp/test_ple.py
174
175
- label: ":computer: (CPU) Qwen4 Exp"
176
key: qwen4-exp-unit-tests-cpu
tests/models/qwen4_exp/test_ple.py
+214
-1
@@ -13,7 +13,7 @@ from torch.nn import functional as F
13
14
import vllm.model_executor.layers.vocab_parallel_embedding as embedding_module
15
import vllm.model_executor.parameter as parameter_module
16
-import vllm.models.qwen4_exp.nvidia.ngram_embedding as ngram_embedding_module
16
+import vllm.models.qwen4_exp.common.ngram_embedding as ngram_embedding_module
17
from vllm.config.quantization import QuantizationConfigArgs
18
from vllm.model_executor.layers.quantization.fp8 import Fp8Config
19
from vllm.model_executor.layers.quantization.modelopt import (
@@ -21,11 +21,16 @@ from vllm.model_executor.layers.quantization.modelopt import (
21
ModelOptNvFp4Config,
22
)
23
from vllm.model_executor.layers.quantization.online.base import OnlineQuantizationConfig
24
+from vllm.models.qwen4_exp.amd import ple_layer as amd_ple_layer
25
+from vllm.models.qwen4_exp.amd.ple_layer import (
26
+ Qwen4ExpPLELayer as Qwen4ExpPLELayerAMD,
27
+)
28
from vllm.models.qwen4_exp.common.ple import (
29
PLEShardOverlap,
30
compute_ple_shard_overlap,
31
copy_ple_embedding_shard_,
32
)
33
+from vllm.models.qwen4_exp.config import Qwen4ExpTextConfig
34
from vllm.models.qwen4_exp.nvidia.ngram_embedding import (
35
Qwen4ExpNGramEmbedding,
36
Qwen4ExpPLEDeviceEmbedding,
@@ -1750,3 +1755,211 @@ def test_fused_gate_correctness(num_tokens: int, strided_kv: bool) -> None:
1755
expected_normed = grouped_norm(expected_gated, norm_conv)
1756
assert torch.equal(gated, expected_gated)
1757
torch.testing.assert_close(normed, expected_normed, atol=1e-2, rtol=1e-2)
1758
+
1759
+
1760
+def _build_amd_ngram_embedding(
1761
+ monkeypatch: pytest.MonkeyPatch,
1762
+ *,
1763
+ device: str,
1764
+ cpu_offload: bool,
1765
+ fp8_checkpoint: bool,
1766
+) -> tuple[amd_ple_layer.Qwen4ExpNGramEmbedding, torch.Tensor]:
1767
+ """Construct and load a small AMD embedding through its checkpoint loader."""
1768
+ _mock_etp_group(monkeypatch)
1769
+ monkeypatch.setattr(embedding_module, "get_tensor_model_parallel_rank", lambda: 0)
1770
+ monkeypatch.setattr(
1771
+ embedding_module, "get_tensor_model_parallel_world_size", lambda: 1
1772
+ )
1773
+ monkeypatch.setattr(parameter_module, "get_tensor_model_parallel_rank", lambda: 0)
1774
+ monkeypatch.setattr(
1775
+ parameter_module, "get_tensor_model_parallel_world_size", lambda: 1
1776
+ )
1777
+
1778
+ monkeypatch.setattr(
1779
+ amd_ple_layer,
1780
+ "get_current_vllm_config",
1781
+ lambda: SimpleNamespace(engram_config=SimpleNamespace(cpu_offload=cpu_offload)),
1782
+ )
1783
+ config = Qwen4ExpTextConfig(
1784
+ ngram_size=3,
1785
+ heads_per_ngram=1,
1786
+ ngram_vocab_size_base=5,
1787
+ make_ngram_vocab_size_divisible_by=8,
1788
+ split_ngram_parts=2,
1789
+ ple_embed_dim=6,
1790
+ eos_token_id=0,
1791
+ vocab_size=64,
1792
+ )
1793
+ with torch.device(device):
1794
+ module = amd_ple_layer.Qwen4ExpNGramEmbedding(
1795
+ config,
1796
+ embedding_dim=config.ple_embed_dim,
1797
+ ple_dense_layer_id=0,
1798
+ max_total_tokens=4,
1799
+ max_num_reqs=2,
1800
+ prefix="test.ple_embedding",
1801
+ layer_name="test.ple",
1802
+ quant_config=(
1803
+ Fp8Config(is_checkpoint_fp8_serialized=True) if fp8_checkpoint else None
1804
+ ),
1805
+ params_dtype=torch.bfloat16,
1806
+ )
1807
+ # Only the outer owner is a stub; the embedding constructor and loader run.
1808
+ layer = Qwen4ExpPLELayerAMD.__new__(Qwen4ExpPLELayerAMD)
1809
+ nn.Module.__init__(layer)
1810
+ layer.ple_embedding = module
1811
+
1812
+ monkeypatch.setattr(
1813
+ amd_ple_layer,
1814
+ "get_forward_context",
1815
+ lambda: SimpleNamespace(no_compile_layers={module.layer_name: layer}),
1816
+ )
1817
+
1818
+ embedding = module.ngram_embedding
1819
+ loaded_weight = (
1820
+ torch.arange(embedding.org_vocab_size * embedding.embedding_dim)
1821
+ .reshape(embedding.org_vocab_size, embedding.embedding_dim)
1822
+ .to(embedding.weight.dtype)
1823
+ )
1824
+ shard_size = (embedding.org_vocab_size + 1) // 2
1825
+ weights = [
1826
+ (f"ngram_embedding.shard_{i}.weight", shard)
1827
+ for i, shard in enumerate(loaded_weight.split(shard_size))
1828
+ ]
1829
+ expected_loaded = {"ngram_embedding.weight"}
1830
+ if fp8_checkpoint:
1831
+ weights.append(("ngram_embedding.weight_scale", torch.tensor([0.25])))
1832
+ expected_loaded.add("ngram_embedding.weight_scale")
1833
+ assert module.load_weights(weights) == expected_loaded
1834
+ embedding.quant_method.process_weights_after_loading(embedding)
1835
+ return module, loaded_weight
1836
+
1837
+
1838
+@pytest.mark.parametrize(
1839
+ "device,cpu_offload",
1840
+ [
1841
+ pytest.param("cpu", False, id="cpu-resident"),
1842
+ pytest.param(
1843
+ "cuda:0",
1844
+ False,
1845
+ id="gpu-resident",
1846
+ marks=pytest.mark.skipif(
1847
+ not torch.cuda.is_available(), reason="requires CUDA or ROCm"
1848
+ ),
1849
+ ),
1850
+ pytest.param(
1851
+ "cuda:0",
1852
+ True,
1853
+ id="gpu-offloaded",
1854
+ marks=pytest.mark.skipif(
1855
+ not torch.cuda.is_available(), reason="requires CUDA or ROCm"
1856
+ ),
1857
+ ),
1858
+ ],
1859
+)
1860
+def test_amd_fp8_embedding_loads_checkpoint_shards_and_global_scale(
1861
+ monkeypatch: pytest.MonkeyPatch,
1862
+ device: str,
1863
+ cpu_offload: bool,
1864
+) -> None:
1865
+ """AMD checkpoint loading must accept and apply the global FP8 scale."""
1866
+ module, loaded_weight = _build_amd_ngram_embedding(
1867
+ monkeypatch, device=device, cpu_offload=cpu_offload, fp8_checkpoint=True
1868
+ )
1869
+ embedding = module.ngram_embedding
1870
+ assert embedding.weight.dtype == torch.float8_e4m3fn
1871
+ assert embedding.weight.device == torch.device("cpu" if cpu_offload else device)
1872
+ if cpu_offload:
1873
+ assert embedding.weight.is_pinned()
1874
+ assert embedding.weight_scale.device == torch.device(device)
1875
+ torch.testing.assert_close(embedding.weight.float().cpu(), loaded_weight.float())
1876
+ dequantized = embedding.dequantize(embedding.weight.to(device), torch.bfloat16)
1877
+ torch.testing.assert_close(
1878
+ dequantized.cpu(), loaded_weight.to(torch.bfloat16) * 0.25, rtol=0, atol=0
1879
+ )
1880
+
1881
+
1882
+@pytest.mark.skipif(not torch.cuda.is_available(), reason="requires CUDA or ROCm")
1883
+@pytest.mark.parametrize("fp8_checkpoint", [False, True], ids=["bf16", "fp8"])
1884
+def test_amd_pinned_embedding_output_written_under_compile(
1885
+ monkeypatch: pytest.MonkeyPatch,
1886
+ fp8_checkpoint: bool,
1887
+) -> None:
1888
+ """The AMD pinned lookup op must survive aot_autograd/Inductor.
1889
+
1890
+ The op writes via the declared-mutated ``output``. Without
1891
+ ``mutates_args=["output"]`` the call is dead-code-eliminated, the UVA lookup
1892
+ never runs, and the returned ``output`` stays untouched.
1893
+ """
1894
+ module, loaded_weight = _build_amd_ngram_embedding(
1895
+ monkeypatch, device="cuda:0", cpu_offload=True, fp8_checkpoint=fp8_checkpoint
1896
+ )
1897
+
1898
+ ngram_ids = torch.tensor([[3, 0], [1, 2]], device="cuda:0")
1899
+ output = torch.zeros(
1900
+ ngram_ids.shape[0],
1901
+ module.embedding_dim,
1902
+ dtype=module.ngram_embedding.weight.dtype,
1903
+ device="cuda:0",
1904
+ )
1905
+
1906
+ def lookup(ids: torch.Tensor, out: torch.Tensor) -> torch.Tensor:
1907
+ torch.ops.vllm.qwen4_exp_amd_ple_ngram_embedding_pinned(
1908
+ ids, out, module.layer_name
1909
+ )
1910
+ return out
1911
+
1912
+ torch.compile(lookup, fullgraph=True)(ngram_ids, output)
1913
+ torch.cuda.current_stream().synchronize()
1914
+
1915
+ expected = loaded_weight[ngram_ids.cpu()].to(device="cuda:0").flatten(-2)
1916
+ torch.testing.assert_close(output.float(), expected.float(), rtol=0, atol=0)
1917
+
1918
+
1919
+@pytest.mark.skipif(not torch.cuda.is_available(), reason="requires CUDA or ROCm")
1920
+@pytest.mark.parametrize("fp8_checkpoint", [False, True], ids=["bf16", "fp8"])
1921
+def test_amd_pinned_embedding_output_written_under_cudagraph_capture(
1922
+ monkeypatch: pytest.MonkeyPatch,
1923
+ fp8_checkpoint: bool,
1924
+) -> None:
1925
+ """The AMD pinned lookup op must be capture-safe.
1926
+
1927
+ It launches a single kernel on the current stream, so a cudagraph capture
1928
+ reproduces the lookup into ``output`` without cross-stream work.
1929
+ """
1930
+ module, loaded_weight = _build_amd_ngram_embedding(
1931
+ monkeypatch, device="cuda:0", cpu_offload=True, fp8_checkpoint=fp8_checkpoint
1932
+ )
1933
+
1934
+ ngram_ids = torch.tensor([[3, 0], [1, 2]], device="cuda:0")
1935
+ output = torch.zeros(
1936
+ ngram_ids.shape[0],
1937
+ module.embedding_dim,
1938
+ dtype=module.ngram_embedding.weight.dtype,
1939
+ device="cuda:0",
1940
+ )
1941
+
1942
+ def lookup(ids: torch.Tensor, out: torch.Tensor) -> torch.Tensor:
1943
+ torch.ops.vllm.qwen4_exp_amd_ple_ngram_embedding_pinned(
1944
+ ids, out, module.layer_name
1945
+ )
1946
+ return out
1947
+
1948
+ graph = torch.cuda.CUDAGraph()
1949
+ warmup_stream = torch.cuda.Stream(device="cuda:0")
1950
+ warmup_stream.wait_stream(torch.cuda.current_stream())
1951
+ with torch.cuda.stream(warmup_stream):
1952
+ for _ in range(3):
1953
+ lookup(ngram_ids, output)
1954
+ torch.cuda.current_stream().wait_stream(warmup_stream)
1955
+
1956
+ with torch.cuda.graph(graph):
1957
+ lookup(ngram_ids, output)
1958
+ for _ in range(2):
1959
+ ngram_ids.copy_(ngram_ids.flip(-1) + 3)
1960
+ output.fill_(float("nan"))
1961
+ graph.replay()
1962
+ torch.cuda.current_stream().synchronize()
1963
+
1964
+ expected = loaded_weight[ngram_ids.cpu()].to(device="cuda:0").flatten(-2)
1965
+ torch.testing.assert_close(output.float(), expected.float(), rtol=0, atol=0)
tests/test_config.py
+20
-14
@@ -1530,26 +1530,32 @@ def test_engram_dp_shared_memory_config_validation(
1530
1531
1532
@pytest.mark.parametrize(
1533
- "architecture, ple_layers, accelerator, supported",
1533
+ "architecture, ple_layers, platform, supported",
1534
[
1535
- ("DeepseekV41ForCausalLM", [1], True, True),
1536
- ("DeepseekV41ForCausalLM", [], True, False),
1537
- ("DeepseekV41ForCausalLM", [1], False, False),
1538
- ("Qwen4ExpForCausalLM", [1], True, True),
1539
- ("Qwen4ExpForConditionalGeneration", [1], True, True),
1540
- ("Qwen4ExpForCausalLM", [], True, False),
1541
- ("Qwen4ExpForCausalLM", None, True, False),
1542
- ("Qwen4ExpForCausalLM", [1], False, False),
1543
- ("LlamaForCausalLM", [1], True, False),
1544
- ("Qwen4ExpMTP", [], True, False),
1545
- (None, None, True, False),
1535
+ ("DeepseekV41ForCausalLM", [1], "cuda", True),
1536
+ ("DeepseekV41ForCausalLM", [1], "rocm", True),
1537
+ ("DeepseekV41ForCausalLM", [], "cuda", False),
1538
+ ("DeepseekV41ForCausalLM", [1], "cpu", False),
1539
+ ("Qwen4ExpForCausalLM", [1], "cuda", True),
1540
+ ("Qwen4ExpForCausalLM", [1], "rocm", True),
1541
+ ("Qwen4ExpForConditionalGeneration", [1], "cuda", True),
1542
+ ("Qwen4ExpForConditionalGeneration", [1], "rocm", True),
1543
+ ("Qwen4ExpForCausalLM", [], "cuda", False),
1544
+ ("Qwen4ExpForCausalLM", None, "cuda", False),
1545
+ ("Qwen4ExpForCausalLM", [1], "cpu", False),
1546
+ ("LlamaForCausalLM", [1], "cuda", False),
1547
+ ("Qwen4ExpMTP", [], "cuda", False),
1548
+ (None, None, "cuda", False),
1549
],
1550
)
1551
def test_engram_model_support(
1549
- monkeypatch, architecture, ple_layers, accelerator, supported
1552
+ monkeypatch, architecture, ple_layers, platform, supported
1553
):
1554
"""A similarly named HF field must not enable unsupported implementations."""
1552
- monkeypatch.setattr(current_platform, "is_cuda_alike", lambda: accelerator)
1555
+ monkeypatch.setattr(current_platform, "is_cuda", lambda: platform == "cuda")
1556
+ monkeypatch.setattr(
1557
+ current_platform, "is_cuda_alike", lambda: platform in ("cuda", "rocm")
1558
+ )
1559
model = (
1560
cast(
1561
ModelConfig,
vllm/config/engram.py
+2
-1
@@ -82,7 +82,8 @@ class EngramConfig:
82
):
83
raise ValueError(
84
"EngramConfig requires a model with supported Engram "
85
- "embeddings, non-empty n-gram layer ids, and CUDA."
85
+ "embeddings, non-empty n-gram layer ids, and a CUDA-alike "
86
+ "device (CUDA or ROCm)."
87
)
88
89
def resolve_dp_shared_memory(self, parallel_config: "ParallelConfig") -> None:
vllm/models/qwen4_exp/amd/ple_layer.py
+90
-4
@@ -11,6 +11,7 @@ from torch import nn
11
12
from vllm.config import CacheConfig, ModelConfig, VllmConfig, get_current_vllm_config
13
from vllm.forward_context import get_forward_context
14
+from vllm.logger import init_logger
15
from vllm.model_executor.layers.linear import ReplicatedLinear
16
from vllm.model_executor.layers.mamba.abstract import MambaBase
17
from vllm.model_executor.layers.mamba.mamba_utils import (
@@ -18,6 +19,9 @@ from vllm.model_executor.layers.mamba.mamba_utils import (
19
MambaStateShapeCalculator,
20
is_conv_state_dim_first,
21
)
22
+from vllm.model_executor.layers.quantization.base_config import (
23
+ QuantizationConfig,
24
+)
25
from vllm.model_executor.models.utils import AutoWeightsLoader
26
from vllm.transformers_utils.configs.qwen4_exp import (
27
Qwen4ExpTextConfig,
@@ -30,7 +34,13 @@ from vllm.v1.attention.backends.short_conv_attn import (
34
)
35
from vllm.v1.attention.backends.utils import NULL_BLOCK_ID
36
33
-from ..common.ple import PLEVocabParallelEmbedding
37
+from ..common.ngram_embedding import (
38
+ Qwen4ExpPLEDeviceEmbedding,
39
+ Qwen4ExpPLEEmbeddingMethod,
40
+ Qwen4ExpPLEPinnedHostEmbedding,
41
+)
42
+
43
+logger = init_logger(__name__)
44
45
46
class Qwen4ExpPLEGroupedNorm(nn.Module):
@@ -172,6 +182,10 @@ class Qwen4ExpNGramEmbedding(nn.Module):
182
max_num_reqs: int,
183
prefix: str,
184
layer_name: str,
185
+ *,
186
+ data_parallel_rank: int = 0,
187
+ quant_config: QuantizationConfig | None = None,
188
+ params_dtype: torch.dtype | None = None,
189
) -> None:
190
super().__init__()
191
self.embedding_dim = embedding_dim
@@ -224,11 +238,39 @@ class Qwen4ExpNGramEmbedding(nn.Module):
238
)
239
divisor = int(config.make_ngram_vocab_size_divisible_by)
240
padded_vocab_size = ((total_vocab_size + divisor - 1) // divisor) * divisor
227
- self.ngram_embedding = PLEVocabParallelEmbedding(
241
+ if params_dtype is None:
242
+ params_dtype = torch.get_default_dtype()
243
+ embedding_prefix = f"{prefix}.ngram_embedding"
244
+ embedding_quant_method = Qwen4ExpPLEEmbeddingMethod.from_quant_config(
245
+ quant_config,
246
+ embedding_prefix,
247
+ getattr(config, "ple_embedding_dtype", None),
248
+ )
249
+ engram_config = get_current_vllm_config().engram_config
250
+ embedding_cls = (
251
+ Qwen4ExpPLEPinnedHostEmbedding
252
+ if engram_config is not None and engram_config.cpu_offload
253
+ else Qwen4ExpPLEDeviceEmbedding
254
+ )
255
+ self.ngram_embedding = embedding_cls(
256
padded_vocab_size,
257
self.head_dim,
258
+ params_dtype=params_dtype,
259
padding_size=divisor,
231
- prefix=f"{prefix}.ngram_embedding",
260
+ prefix=embedding_prefix,
261
+ embedding_method=embedding_quant_method,
262
+ num_ngram_heads=self.ngram_heads,
263
+ max_total_tokens=max_total_tokens,
264
+ data_parallel_rank=data_parallel_rank,
265
+ )
266
+ logger.info(
267
+ "Initialized AMD PLE embedding %s: quantization_method=%s, "
268
+ "weight_dtype=%s, weight_device=%s, pinned=%s",
269
+ embedding_prefix,
270
+ type(embedding_quant_method).__name__,
271
+ self.ngram_embedding.weight.dtype,
272
+ self.ngram_embedding.weight.device,
273
+ self.ngram_embedding.weight.is_pinned(),
274
)
275
self.register_buffer(
276
"positions_buffer",
@@ -344,9 +386,21 @@ class Qwen4ExpNGramEmbedding(nn.Module):
386
ids = torch.remainder(mixed.unsqueeze(-1), sizes) + offsets
387
id_blocks.append(ids[request_indices, adjusted_columns])
388
ngram_ids = torch.cat(id_blocks, dim=-1)
389
+ embedding = self.ngram_embedding
390
+ if embedding.supports_prefetch:
391
+ output = ngram_ids.new_empty(
392
+ (ngram_ids.shape[0], self.embedding_dim),
393
+ dtype=embedding.weight.dtype,
394
+ )
395
+ torch.ops.vllm.qwen4_exp_amd_ple_ngram_embedding_pinned(
396
+ ngram_ids,
397
+ output,
398
+ self.layer_name,
399
+ )
400
+ return output
401
output = ngram_ids.new_empty(
402
(ngram_ids.shape[0], self.embedding_dim),
349
- dtype=self.ngram_embedding.params_dtype,
403
+ dtype=embedding.params_dtype,
404
)
405
torch.ops.vllm.qwen4_exp_amd_ple_ngram_embedding(
406
ngram_ids,
@@ -459,6 +513,9 @@ class Qwen4ExpPLELayer(nn.Module, MambaBase):
513
vllm_config.scheduler_config.max_num_seqs,
514
f"{prefix}.ple_embedding",
515
prefix,
516
+ data_parallel_rank=vllm_config.parallel_config.data_parallel_rank,
517
+ quant_config=quant_config,
518
+ params_dtype=model_config.dtype,
519
)
520
self.key_proj = ReplicatedLinear(
521
int(config.ple_embed_dim),
@@ -1044,6 +1101,10 @@ class Qwen4ExpPLELayer(nn.Module, MambaBase):
1101
f"{hidden_states.shape[0]}"
1102
)
1103
embeddings = self.ple_embedding(input_ids, query_start_loc, ngram_context)
1104
+ embeddings = self.ple_embedding.ngram_embedding.dequantize(
1105
+ embeddings,
1106
+ self.ple_embedding.ngram_embedding.params_dtype,
1107
+ )
1108
key, _ = self.key_proj(embeddings)
1109
value, _ = self.value_proj(embeddings)
1110
token_count = hidden_states.shape[0]
@@ -1081,6 +1142,24 @@ def qwen4_exp_amd_ple_ngram_embedding(
1142
output.copy_(result)
1143
1144
1145
+def qwen4_exp_amd_ple_ngram_embedding_pinned(
1146
+ ngram_ids: torch.Tensor,
1147
+ output: torch.Tensor,
1148
+ layer_name: str,
1149
+) -> None:
1150
+ """Run the pinned PLE UVA lookup outside Inductor's FX graph.
1151
+
1152
+ Same rationale as the device-path escape: keeping the large embedding weight
1153
+ out of the graph prevents AOT compile-time autotuning from materializing a
1154
+ synthetic copy of the weight.
1155
+ """
1156
+ layer = get_forward_context().no_compile_layers[layer_name]
1157
+ if not isinstance(layer, Qwen4ExpPLELayer):
1158
+ raise TypeError(f"{layer_name} is not a Qwen4Exp PLE owner")
1159
+ result = layer.ple_embedding.ngram_embedding.sync_lookup(ngram_ids).flatten(-2)
1160
+ output.copy_(result)
1161
+
1162
+
1163
def qwen4_exp_ple_short_conv(
1164
inputs: torch.Tensor,
1165
output: torch.Tensor,
@@ -1098,6 +1177,13 @@ direct_register_custom_op(
1177
)
1178
1179
1180
+direct_register_custom_op(
1181
+ op_name="qwen4_exp_amd_ple_ngram_embedding_pinned",
1182
+ op_func=qwen4_exp_amd_ple_ngram_embedding_pinned,
1183
+ mutates_args=["output"],
1184
+)
1185
+
1186
+
1187
direct_register_custom_op(
1188
op_name="qwen4_exp_ple_short_conv",
1189
op_func=qwen4_exp_ple_short_conv,
vllm/models/qwen4_exp/common/ngram_embedding.py
new
+542
@@ -0,0 +1,542 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+"""Shared Qwen4Exp n-gram embedding storage with device and pinned-host backends.
4
+
5
+Both the NVIDIA and AMD Qwen4Exp implementations use these classes so the (large)
6
+n-gram embedding table can be kept in pinned host memory and looked up through
7
+Unified Virtual Addressing on any CUDA-alike platform.
8
+"""
9
+
10
+from abc import ABC, abstractmethod
11
+from typing import ClassVar
12
+
13
+import torch
14
+import torch.nn.functional as F
15
+from torch import nn
16
+
17
+from vllm.compilation.breakable_cudagraph import eager_break_during_capture
18
+from vllm.distributed import get_dp_group, get_etp_group, get_tp_group
19
+from vllm.forward_context import DPMetadata, get_forward_context
20
+from vllm.logger import init_logger
21
+from vllm.model_executor.layers.quantization.base_config import (
22
+ QuantizationConfig,
23
+ QuantizeMethodBase,
24
+)
25
+from vllm.model_executor.layers.quantization.fp8 import Fp8Config
26
+from vllm.model_executor.layers.quantization.modelopt import (
27
+ ModelOptMixedPrecisionConfig,
28
+ ModelOptQuantConfigBase,
29
+)
30
+from vllm.model_executor.layers.quantization.utils.fp8_utils import (
31
+ create_fp8_scale_parameter,
32
+)
33
+from vllm.model_executor.layers.quantization.utils.quant_utils import (
34
+ is_layer_skipped,
35
+)
36
+from vllm.model_executor.parameter import (
37
+ ModelWeightParameter,
38
+ PerTensorScaleParameter,
39
+)
40
+from vllm.model_executor.utils import set_weight_attrs
41
+from vllm.triton_utils import tl, triton
42
+from vllm.utils.platform_utils import is_uva_available
43
+from vllm.utils.torch_utils import get_accelerator_view_from_cpu_tensor
44
+
45
+from .ple import PLEVocabParallelEmbedding
46
+
47
+logger = init_logger(__name__)
48
+
49
+
50
+class Qwen4ExpPLEEmbedding(PLEVocabParallelEmbedding, ABC):
51
+ """ETP-sharded PLE table shared by device and pinned-host backends."""
52
+
53
+ supports_prefetch: ClassVar[bool] = False
54
+
55
+ def __init__(
56
+ self,
57
+ num_embeddings: int,
58
+ embedding_dim: int,
59
+ *,
60
+ params_dtype: torch.dtype,
61
+ padding_size: int,
62
+ prefix: str,
63
+ embedding_method: "Qwen4ExpPLEEmbeddingMethod",
64
+ num_ngram_heads: int = 1,
65
+ max_total_tokens: int = 0,
66
+ data_parallel_rank: int = 0,
67
+ ) -> None:
68
+ del num_ngram_heads, max_total_tokens
69
+ super().__init__(
70
+ num_embeddings,
71
+ embedding_dim,
72
+ params_dtype=params_dtype,
73
+ padding_size=padding_size,
74
+ prefix=prefix,
75
+ quant_method=embedding_method,
76
+ parallel_group=get_etp_group(),
77
+ )
78
+ self.embedding_method = embedding_method
79
+ self.data_parallel_rank = data_parallel_rank
80
+ tp_size = get_tp_group().world_size
81
+ if self.tp_size % tp_size:
82
+ raise ValueError(
83
+ "ETP size must be divisible by TP size, but got "
84
+ f"ETP={self.tp_size} and TP={tp_size}"
85
+ )
86
+ self.etp_data_parallel_size = self.tp_size // tp_size
87
+
88
+ @abstractmethod
89
+ def allocate_embedding_weight(
90
+ self,
91
+ num_embeddings: int,
92
+ embedding_dim: int,
93
+ dtype: torch.dtype,
94
+ ) -> torch.Tensor:
95
+ """Allocate storage for the complete embedding weight."""
96
+ raise NotImplementedError
97
+
98
+ def dequantize(
99
+ self,
100
+ embeddings: torch.Tensor,
101
+ output_dtype: torch.dtype,
102
+ ) -> torch.Tensor:
103
+ """Delegate storage-format conversion to the embedding method."""
104
+ return self.embedding_method.dequantize(self, embeddings, output_dtype)
105
+
106
+ def _get_dp_gather_slot(self, local_num_tokens: int) -> tuple[int, int]:
107
+ """Return the per-DP slot size and this rank's slot offset."""
108
+ if self.etp_data_parallel_size == 1:
109
+ return local_num_tokens, 0
110
+ dp_metadata: DPMetadata | None = get_forward_context().dp_metadata
111
+ if dp_metadata is None:
112
+ raise RuntimeError("ETP spanning DP requires DP token metadata")
113
+ group_start = (self.data_parallel_rank // self.etp_data_parallel_size) * (
114
+ self.etp_data_parallel_size
115
+ )
116
+ group_end = group_start + self.etp_data_parallel_size
117
+ token_counts = dp_metadata.num_tokens_across_dp_cpu.tolist()
118
+ group_counts = token_counts[group_start:group_end]
119
+ slot_size = max(group_counts)
120
+ dp_rank = get_dp_group().rank_in_group
121
+ return slot_size, dp_rank * slot_size
122
+
123
+ def _gather_dp_ids(
124
+ self,
125
+ ngram_ids: torch.Tensor,
126
+ slot_size: int,
127
+ ) -> torch.Tensor:
128
+ """Gather DP-local IDs that share one ETP-sharded PLE table."""
129
+ if self.etp_data_parallel_size == 1:
130
+ return ngram_ids
131
+ if ngram_ids.shape[0] < slot_size:
132
+ padding = ngram_ids.new_zeros(
133
+ slot_size - ngram_ids.shape[0], ngram_ids.shape[1]
134
+ )
135
+ ngram_ids = torch.cat((ngram_ids, padding), dim=0)
136
+ return get_dp_group().all_gather(ngram_ids, dim=0)
137
+
138
+ def _select_embeddings(
139
+ self,
140
+ embeddings: torch.Tensor,
141
+ local_num_tokens: int,
142
+ slot_offset: int,
143
+ ) -> torch.Tensor:
144
+ """Select this DP rank's rows from the ETP-reduced embeddings."""
145
+ if self.etp_data_parallel_size == 1:
146
+ return embeddings
147
+ return embeddings.narrow(0, slot_offset, local_num_tokens)
148
+
149
+ @abstractmethod
150
+ def start_prefetch(
151
+ self,
152
+ hidden_states: torch.Tensor,
153
+ ngram_ids: torch.Tensor,
154
+ ) -> None:
155
+ """Start an asynchronous lookup when supported."""
156
+ raise NotImplementedError
157
+
158
+
159
+class Qwen4ExpPLEEmbeddingMethod(QuantizeMethodBase):
160
+ """Quantization interface shared by resident and pinned PLE tables."""
161
+
162
+ # PLE post-load processing only validates scales in their current storage.
163
+ requires_device_loading: bool = False
164
+
165
+ @staticmethod
166
+ def from_quant_config(
167
+ quant_config: QuantizationConfig | None,
168
+ prefix: str,
169
+ embedding_dtype: str | None = None,
170
+ ) -> "Qwen4ExpPLEEmbeddingMethod":
171
+ """Select the concrete PLE embedding format for a layer."""
172
+ if embedding_dtype == "float8_e4m3fn":
173
+ return Qwen4ExpPLEFp8EmbeddingMethod()
174
+ if quant_config is None:
175
+ return Qwen4ExpPLEUnquantizedEmbeddingMethod()
176
+ if isinstance(quant_config, ModelOptMixedPrecisionConfig):
177
+ if quant_config._resolve_quant_algo(prefix) == "FP8":
178
+ return Qwen4ExpPLEFp8EmbeddingMethod()
179
+ return Qwen4ExpPLEUnquantizedEmbeddingMethod()
180
+ if isinstance(
181
+ quant_config, ModelOptQuantConfigBase
182
+ ) and quant_config.is_layer_excluded(prefix):
183
+ return Qwen4ExpPLEUnquantizedEmbeddingMethod()
184
+ if not isinstance(quant_config, Fp8Config):
185
+ raise NotImplementedError(
186
+ "Qwen4Exp PLE embedding does not support quantization config "
187
+ f"{type(quant_config).__name__}"
188
+ )
189
+
190
+ ignored_layers = quant_config.ignored_layers
191
+ if is_layer_skipped(
192
+ prefix,
193
+ ignored_layers,
194
+ quant_config.packed_modules_mapping,
195
+ match_mode=quant_config.ignored_layers_match_mode,
196
+ ):
197
+ return Qwen4ExpPLEUnquantizedEmbeddingMethod()
198
+ # PLE checkpoint shards form one runtime embedding parameter.
199
+ shard_prefix = f"{prefix}.shard_"
200
+ if any(name.startswith(shard_prefix) for name in ignored_layers):
201
+ return Qwen4ExpPLEUnquantizedEmbeddingMethod()
202
+ if not quant_config.is_checkpoint_fp8_serialized:
203
+ raise NotImplementedError(
204
+ "Qwen4Exp PLE embedding only supports serialized FP8 checkpoints"
205
+ )
206
+ return Qwen4ExpPLEFp8EmbeddingMethod()
207
+
208
+ def apply(
209
+ self,
210
+ layer: nn.Module,
211
+ x: torch.Tensor,
212
+ bias: torch.Tensor | None = None,
213
+ ) -> torch.Tensor:
214
+ raise NotImplementedError("PLE weights only support embedding lookup")
215
+
216
+ def embedding(self, layer: nn.Module, input_: torch.Tensor) -> torch.Tensor:
217
+ return F.embedding(input_, layer.weight)
218
+
219
+ @abstractmethod
220
+ def dequantize(
221
+ self,
222
+ layer: nn.Module,
223
+ embeddings: torch.Tensor,
224
+ output_dtype: torch.dtype,
225
+ ) -> torch.Tensor:
226
+ """Convert looked-up PLE rows to the activation dtype."""
227
+ raise NotImplementedError
228
+
229
+
230
+class Qwen4ExpPLEUnquantizedEmbeddingMethod(Qwen4ExpPLEEmbeddingMethod):
231
+ """Unquantized PLE embedding storage and lookup semantics."""
232
+
233
+ def create_weights(
234
+ self,
235
+ layer: Qwen4ExpPLEEmbedding,
236
+ input_size_per_partition: int,
237
+ output_partition_sizes: list[int],
238
+ input_size: int,
239
+ output_size: int,
240
+ params_dtype: torch.dtype,
241
+ **extra_weight_attrs,
242
+ ) -> None:
243
+ del input_size, output_size
244
+ weight = nn.Parameter(
245
+ layer.allocate_embedding_weight(
246
+ sum(output_partition_sizes),
247
+ input_size_per_partition,
248
+ params_dtype,
249
+ ),
250
+ requires_grad=False,
251
+ )
252
+ set_weight_attrs(weight, {"input_dim": 1, "output_dim": 0})
253
+ set_weight_attrs(weight, extra_weight_attrs)
254
+ layer.register_parameter("weight", weight)
255
+
256
+ def dequantize(
257
+ self,
258
+ layer: nn.Module,
259
+ embeddings: torch.Tensor,
260
+ output_dtype: torch.dtype,
261
+ ) -> torch.Tensor:
262
+ del layer, output_dtype
263
+ return embeddings
264
+
265
+
266
+class Qwen4ExpPLEFp8EmbeddingMethod(Qwen4ExpPLEEmbeddingMethod):
267
+ """FP8 PLE embedding with one global checkpoint scale."""
268
+
269
+ def create_weights(
270
+ self,
271
+ layer: Qwen4ExpPLEEmbedding,
272
+ input_size_per_partition: int,
273
+ output_partition_sizes: list[int],
274
+ input_size: int,
275
+ output_size: int,
276
+ params_dtype: torch.dtype,
277
+ **extra_weight_attrs,
278
+ ) -> None:
279
+ del input_size, output_size, params_dtype
280
+ weight_loader = extra_weight_attrs.get("weight_loader")
281
+ weight = ModelWeightParameter(
282
+ data=layer.allocate_embedding_weight(
283
+ sum(output_partition_sizes),
284
+ input_size_per_partition,
285
+ torch.float8_e4m3fn,
286
+ ),
287
+ input_dim=1,
288
+ output_dim=0,
289
+ weight_loader=weight_loader,
290
+ )
291
+ layer.register_parameter("weight", weight)
292
+
293
+ weight_scale = create_fp8_scale_parameter(
294
+ PerTensorScaleParameter,
295
+ output_partition_sizes,
296
+ input_size_per_partition,
297
+ None,
298
+ weight_loader,
299
+ scale_dtype=torch.float32,
300
+ )
301
+ layer.register_parameter("weight_scale", weight_scale)
302
+
303
+ def process_weights_after_loading(self, layer: nn.Module) -> None:
304
+ """Reject FP8 PLE checkpoints without a global scale."""
305
+ sentinel = torch.finfo(torch.float32).min
306
+ if torch.any(layer.weight_scale == sentinel):
307
+ raise ValueError("FP8 PLE checkpoint is missing its global scale")
308
+
309
+ def dequantize(
310
+ self,
311
+ layer: nn.Module,
312
+ embeddings: torch.Tensor,
313
+ output_dtype: torch.dtype,
314
+ ) -> torch.Tensor:
315
+ weight_scale = getattr(layer, "weight_scale", None)
316
+ if weight_scale is None:
317
+ raise RuntimeError("FP8 PLE embedding is missing its global scale")
318
+ if weight_scale.device != embeddings.device:
319
+ raise RuntimeError("FP8 PLE embedding scale must be on the output device")
320
+ return embeddings.to(output_dtype) * weight_scale.to(output_dtype)
321
+
322
+
323
+class Qwen4ExpPLEDeviceEmbedding(Qwen4ExpPLEEmbedding):
324
+ """PLE table allocated on the active model device."""
325
+
326
+ def allocate_embedding_weight(
327
+ self,
328
+ num_embeddings: int,
329
+ embedding_dim: int,
330
+ dtype: torch.dtype,
331
+ ) -> torch.Tensor:
332
+ """Allocate the complete PLE weight on the active device."""
333
+ return torch.empty(num_embeddings, embedding_dim, dtype=dtype)
334
+
335
+ def start_prefetch(
336
+ self,
337
+ hidden_states: torch.Tensor,
338
+ ngram_ids: torch.Tensor,
339
+ ) -> None:
340
+ """Resident embedding prefetch is a no-op."""
341
+ return None
342
+
343
+ def forward(self, ngram_ids: torch.Tensor) -> torch.Tensor:
344
+ """Gather ETP inputs, look up embeddings, and select local rows."""
345
+ slot_size, slot_offset = self._get_dp_gather_slot(ngram_ids.shape[0])
346
+ gathered_ids = self._gather_dp_ids(ngram_ids, slot_size)
347
+ embeddings = super().forward(gathered_ids)
348
+ return self._select_embeddings(
349
+ embeddings,
350
+ ngram_ids.shape[0],
351
+ slot_offset,
352
+ )
353
+
354
+
355
+@triton.jit
356
+def _lookup_ple_embedding_from_pinned_kernel(
357
+ weight_ptr,
358
+ ids_ptr,
359
+ output_ptr,
360
+ embedding_dim,
361
+ tp_vocab_start,
362
+ tp_vocab_end,
363
+ BLOCK_D: tl.constexpr,
364
+):
365
+ """Look up TP-owned PLE rows through a CUDA view of pinned host memory."""
366
+ row_id = tl.program_id(0)
367
+ global_idx = tl.load(ids_ptr + row_id)
368
+ in_range = (global_idx >= tp_vocab_start) & (global_idx < tp_vocab_end)
369
+ local_idx = tl.where(in_range, global_idx - tp_vocab_start, 0)
370
+ offsets = tl.arange(0, BLOCK_D)
371
+ store_mask = offsets < embedding_dim
372
+ load_mask = store_mask & in_range
373
+ values = tl.load(
374
+ weight_ptr + local_idx * embedding_dim + offsets,
375
+ mask=load_mask,
376
+ other=0.0,
377
+ )
378
+ tl.store(
379
+ output_ptr + row_id * embedding_dim + offsets,
380
+ values,
381
+ mask=store_mask,
382
+ )
383
+
384
+
385
+class Qwen4ExpPLEPinnedHostEmbedding(Qwen4ExpPLEEmbedding):
386
+ """PLE table loaded into pinned CPU memory and looked up through UVA."""
387
+
388
+ supports_prefetch: ClassVar[bool] = True
389
+
390
+ def __init__(
391
+ self,
392
+ num_embeddings: int,
393
+ embedding_dim: int,
394
+ *,
395
+ params_dtype: torch.dtype,
396
+ padding_size: int,
397
+ prefix: str,
398
+ embedding_method: Qwen4ExpPLEEmbeddingMethod,
399
+ num_ngram_heads: int = 1,
400
+ max_total_tokens: int = 0,
401
+ data_parallel_rank: int = 0,
402
+ ) -> None:
403
+ if not is_uva_available():
404
+ raise RuntimeError("Engram CPU offload requires UVA support")
405
+ super().__init__(
406
+ num_embeddings,
407
+ embedding_dim,
408
+ params_dtype=params_dtype,
409
+ padding_size=padding_size,
410
+ prefix=prefix,
411
+ embedding_method=embedding_method,
412
+ num_ngram_heads=num_ngram_heads,
413
+ max_total_tokens=max_total_tokens,
414
+ data_parallel_rank=data_parallel_rank,
415
+ )
416
+ self._uva_weight = get_accelerator_view_from_cpu_tensor(self.weight)
417
+ self._block_d = triton.next_power_of_2(self.embedding_dim)
418
+ self._prefetch_stream = torch.cuda.Stream(device=self._uva_weight.device)
419
+ self._prefetch_buffer = torch.empty(
420
+ max_total_tokens * self.etp_data_parallel_size,
421
+ num_ngram_heads,
422
+ self.embedding_dim,
423
+ dtype=self.weight.dtype,
424
+ device=self._uva_weight.device,
425
+ )
426
+ self._output_dim = num_ngram_heads * self.embedding_dim
427
+
428
+ def allocate_embedding_weight(
429
+ self,
430
+ num_embeddings: int,
431
+ embedding_dim: int,
432
+ dtype: torch.dtype,
433
+ ) -> torch.Tensor:
434
+ """Allocate the complete PLE weight directly in pinned CPU memory."""
435
+ return torch.empty(
436
+ num_embeddings,
437
+ embedding_dim,
438
+ dtype=dtype,
439
+ device="cpu",
440
+ pin_memory=True,
441
+ )
442
+
443
+ def _lookup(
444
+ self,
445
+ input_ids: torch.Tensor,
446
+ output: torch.Tensor | None = None,
447
+ ) -> torch.Tensor:
448
+ """Look up local ETP rows while preserving the weight storage dtype."""
449
+ expected_shape = (*input_ids.shape, self.embedding_dim)
450
+ if output is None:
451
+ output = torch.empty(
452
+ expected_shape,
453
+ dtype=self.weight.dtype,
454
+ device=input_ids.device,
455
+ )
456
+ elif (
457
+ tuple(output.shape) != expected_shape
458
+ or output.dtype != self.weight.dtype
459
+ or output.device != input_ids.device
460
+ ):
461
+ raise ValueError(
462
+ "PLE prefetch output must match the input shape, weight dtype, "
463
+ "and input device"
464
+ )
465
+
466
+ flat_ids = input_ids.reshape(-1).long()
467
+ if flat_ids.numel():
468
+ _lookup_ple_embedding_from_pinned_kernel[(flat_ids.numel(),)](
469
+ self._uva_weight,
470
+ flat_ids,
471
+ output,
472
+ self.embedding_dim,
473
+ self.shard_indices.org_vocab_start_index,
474
+ self.shard_indices.org_vocab_end_index,
475
+ BLOCK_D=self._block_d,
476
+ )
477
+ return output
478
+
479
+ def sync_lookup(self, ngram_ids: torch.Tensor) -> torch.Tensor:
480
+ """Synchronous UVA lookup for platforms without prefetch wiring."""
481
+ slot_size, slot_offset = self._get_dp_gather_slot(ngram_ids.shape[0])
482
+ gathered_ids = self._gather_dp_ids(ngram_ids, slot_size)
483
+ embeddings = self._lookup(gathered_ids)
484
+ embeddings = self._reduce_etp_embeddings(embeddings)
485
+ return self._select_embeddings(
486
+ embeddings,
487
+ ngram_ids.shape[0],
488
+ slot_offset,
489
+ )
490
+
491
+ def _reduce_etp_embeddings(self, embeddings: torch.Tensor) -> torch.Tensor:
492
+ """Combine pinned lookup results owned by different ETP ranks."""
493
+ if self.tp_size == 1:
494
+ return embeddings
495
+ assert self.parallel_group is not None
496
+ if embeddings.dtype in (torch.float8_e4m3fn, torch.float8_e5m2):
497
+ # Each vocabulary row has one owner, so reduce the raw FP8 bytes.
498
+ reduced = self.parallel_group.all_reduce(embeddings.view(torch.int8))
499
+ return reduced.view(embeddings.dtype)
500
+ return self.parallel_group.all_reduce(embeddings)
501
+
502
+ @eager_break_during_capture
503
+ def start_prefetch(
504
+ self,
505
+ hidden_states: torch.Tensor,
506
+ ngram_ids: torch.Tensor,
507
+ ) -> None:
508
+ """Gather ETP IDs and launch their UVA lookup on the side stream."""
509
+ slot_size, _ = self._get_dp_gather_slot(ngram_ids.shape[0])
510
+ gathered_ids = self._gather_dp_ids(ngram_ids, slot_size)
511
+ active_output = self._prefetch_buffer[: gathered_ids.shape[0]]
512
+ prefetch_stream = self._prefetch_stream
513
+ prefetch_stream.wait_stream(torch.cuda.current_stream())
514
+ gathered_ids.record_stream(prefetch_stream)
515
+ with torch.cuda.stream(prefetch_stream):
516
+ self._lookup(gathered_ids, output=active_output)
517
+
518
+ @eager_break_during_capture
519
+ def _finalize_prefetch(
520
+ self,
521
+ prefetch_output: torch.Tensor,
522
+ output: torch.Tensor,
523
+ ) -> None:
524
+ """Join the side stream, reduce ETP shards, and select local rows."""
525
+ torch.cuda.current_stream().wait_stream(self._prefetch_stream)
526
+ slot_size, slot_offset = self._get_dp_gather_slot(output.shape[0])
527
+ active_output = prefetch_output[: slot_size * self.etp_data_parallel_size]
528
+ embeddings = self._reduce_etp_embeddings(active_output)
529
+ embeddings = self._select_embeddings(
530
+ embeddings,
531
+ output.shape[0],
532
+ slot_offset,
533
+ )
534
+ output.copy_(embeddings.flatten(-2))
535
+
536
+ def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
537
+ """Finish the pinned lookup into graph-owned output storage."""
538
+ output = self._prefetch_buffer.new_empty(
539
+ (hidden_states.shape[0], self._output_dim)
540
+ )
541
+ self._finalize_prefetch(self._prefetch_buffer, output)
542
+ return output
vllm/models/qwen4_exp/nvidia/ngram_embedding.py
+17
-509
@@ -2,534 +2,42 @@
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
"""Qwen4Exp n-gram embeddings with device and pinned-host storage."""
4
5
-from abc import ABC, abstractmethod
5
from collections.abc import Iterable
7
-from typing import ClassVar
6
7
import torch
10
-import torch.nn.functional as F
8
from torch import nn
9
13
-from vllm.compilation.breakable_cudagraph import eager_break_during_capture
10
from vllm.config import get_current_vllm_config
15
-from vllm.distributed import get_dp_group, get_etp_group, get_tp_group
16
-from vllm.forward_context import DPMetadata, get_forward_context
11
from vllm.logger import init_logger
12
from vllm.model_executor.layers.quantization.base_config import (
13
QuantizationConfig,
20
- QuantizeMethodBase,
21
-)
22
-from vllm.model_executor.layers.quantization.fp8 import Fp8Config
23
-from vllm.model_executor.layers.quantization.modelopt import (
24
- ModelOptMixedPrecisionConfig,
25
- ModelOptQuantConfigBase,
26
-)
27
-from vllm.model_executor.layers.quantization.utils.fp8_utils import (
28
- create_fp8_scale_parameter,
29
-)
30
-from vllm.model_executor.layers.quantization.utils.quant_utils import (
31
- is_layer_skipped,
14
)
15
from vllm.model_executor.models.utils import AutoWeightsLoader
34
-from vllm.model_executor.parameter import (
35
- ModelWeightParameter,
36
- PerTensorScaleParameter,
37
-)
38
-from vllm.model_executor.utils import set_weight_attrs
16
from vllm.transformers_utils.configs.qwen4_exp import (
17
Qwen4ExpTextConfig,
18
)
42
-from vllm.triton_utils import tl, triton
43
-from vllm.utils.platform_utils import is_uva_available
44
-from vllm.utils.torch_utils import get_accelerator_view_from_cpu_tensor
19
46
-from ..common.ple import PLEVocabParallelEmbedding
20
+from ..common.ngram_embedding import (
21
+ Qwen4ExpPLEDeviceEmbedding,
22
+ Qwen4ExpPLEEmbedding,
23
+ Qwen4ExpPLEEmbeddingMethod,
24
+ Qwen4ExpPLEFp8EmbeddingMethod,
25
+ Qwen4ExpPLEPinnedHostEmbedding,
26
+ Qwen4ExpPLEUnquantizedEmbeddingMethod,
27
+)
28
from .ops.ple import ple_ngram_ids
29
30
logger = init_logger(__name__)
31
51
-
52
-class Qwen4ExpPLEEmbedding(PLEVocabParallelEmbedding, ABC):
53
- """ETP-sharded PLE table shared by device and pinned-host backends."""
54
-
55
- supports_prefetch: ClassVar[bool] = False
56
-
57
- def __init__(
58
- self,
59
- num_embeddings: int,
60
- embedding_dim: int,
61
- *,
62
- params_dtype: torch.dtype,
63
- padding_size: int,
64
- prefix: str,
65
- embedding_method: "Qwen4ExpPLEEmbeddingMethod",
66
- num_ngram_heads: int = 1,
67
- max_total_tokens: int = 0,
68
- data_parallel_rank: int = 0,
69
- ) -> None:
70
- del num_ngram_heads, max_total_tokens
71
- super().__init__(
72
- num_embeddings,
73
- embedding_dim,
74
- params_dtype=params_dtype,
75
- padding_size=padding_size,
76
- prefix=prefix,
77
- quant_method=embedding_method,
78
- parallel_group=get_etp_group(),
79
- )
80
- self.embedding_method = embedding_method
81
- self.data_parallel_rank = data_parallel_rank
82
- tp_size = get_tp_group().world_size
83
- if self.tp_size % tp_size:
84
- raise ValueError(
85
- "ETP size must be divisible by TP size, but got "
86
- f"ETP={self.tp_size} and TP={tp_size}"
87
- )
88
- self.etp_data_parallel_size = self.tp_size // tp_size
89
-
90
- @abstractmethod
91
- def allocate_embedding_weight(
92
- self,
93
- num_embeddings: int,
94
- embedding_dim: int,
95
- dtype: torch.dtype,
96
- ) -> torch.Tensor:
97
- """Allocate storage for the complete embedding weight."""
98
- raise NotImplementedError
99
-
100
- def dequantize(
101
- self,
102
- embeddings: torch.Tensor,
103
- output_dtype: torch.dtype,
104
- ) -> torch.Tensor:
105
- """Delegate storage-format conversion to the embedding method."""
106
- return self.embedding_method.dequantize(self, embeddings, output_dtype)
107
-
108
- def _get_dp_gather_slot(self, local_num_tokens: int) -> tuple[int, int]:
109
- """Return the per-DP slot size and this rank's slot offset."""
110
- if self.etp_data_parallel_size == 1:
111
- return local_num_tokens, 0
112
- dp_metadata: DPMetadata | None = get_forward_context().dp_metadata
113
- if dp_metadata is None:
114
- raise RuntimeError("ETP spanning DP requires DP token metadata")
115
- group_start = (self.data_parallel_rank // self.etp_data_parallel_size) * (
116
- self.etp_data_parallel_size
117
- )
118
- group_end = group_start + self.etp_data_parallel_size
119
- token_counts = dp_metadata.num_tokens_across_dp_cpu.tolist()
120
- group_counts = token_counts[group_start:group_end]
121
- slot_size = max(group_counts)
122
- dp_rank = get_dp_group().rank_in_group
123
- return slot_size, dp_rank * slot_size
124
-
125
- def _gather_dp_ids(
126
- self,
127
- ngram_ids: torch.Tensor,
128
- slot_size: int,
129
- ) -> torch.Tensor:
130
- """Gather DP-local IDs that share one ETP-sharded PLE table."""
131
- if self.etp_data_parallel_size == 1:
132
- return ngram_ids
133
- if ngram_ids.shape[0] < slot_size:
134
- padding = ngram_ids.new_zeros(
135
- slot_size - ngram_ids.shape[0], ngram_ids.shape[1]
136
- )
137
- ngram_ids = torch.cat((ngram_ids, padding), dim=0)
138
- return get_dp_group().all_gather(ngram_ids, dim=0)
139
-
140
- def _select_embeddings(
141
- self,
142
- embeddings: torch.Tensor,
143
- local_num_tokens: int,
144
- slot_offset: int,
145
- ) -> torch.Tensor:
146
- """Select this DP rank's rows from the ETP-reduced embeddings."""
147
- if self.etp_data_parallel_size == 1:
148
- return embeddings
149
- return embeddings.narrow(0, slot_offset, local_num_tokens)
150
-
151
- @abstractmethod
152
- def start_prefetch(
153
- self,
154
- hidden_states: torch.Tensor,
155
- ngram_ids: torch.Tensor,
156
- ) -> None:
157
- """Start an asynchronous lookup when supported."""
158
- raise NotImplementedError
159
-
160
-
161
-class Qwen4ExpPLEEmbeddingMethod(QuantizeMethodBase):
162
- """Quantization interface shared by resident and pinned PLE tables."""
163
-
164
- # PLE post-load processing only validates scales in their current storage.
165
- requires_device_loading: bool = False
166
-
167
- @staticmethod
168
- def from_quant_config(
169
- quant_config: QuantizationConfig | None,
170
- prefix: str,
171
- embedding_dtype: str | None = None,
172
- ) -> "Qwen4ExpPLEEmbeddingMethod":
173
- """Select the concrete PLE embedding format for a layer."""
174
- if embedding_dtype == "float8_e4m3fn":
175
- return Qwen4ExpPLEFp8EmbeddingMethod()
176
- if quant_config is None:
177
- return Qwen4ExpPLEUnquantizedEmbeddingMethod()
178
- if isinstance(quant_config, ModelOptMixedPrecisionConfig):
179
- if quant_config._resolve_quant_algo(prefix) == "FP8":
180
- return Qwen4ExpPLEFp8EmbeddingMethod()
181
- return Qwen4ExpPLEUnquantizedEmbeddingMethod()
182
- if isinstance(
183
- quant_config, ModelOptQuantConfigBase
184
- ) and quant_config.is_layer_excluded(prefix):
185
- return Qwen4ExpPLEUnquantizedEmbeddingMethod()
186
- if not isinstance(quant_config, Fp8Config):
187
- raise NotImplementedError(
188
- "Qwen4Exp PLE embedding does not support quantization config "
189
- f"{type(quant_config).__name__}"
190
- )
191
-
192
- ignored_layers = quant_config.ignored_layers
193
- if is_layer_skipped(
194
- prefix,
195
- ignored_layers,
196
- quant_config.packed_modules_mapping,
197
- match_mode=quant_config.ignored_layers_match_mode,
198
- ):
199
- return Qwen4ExpPLEUnquantizedEmbeddingMethod()
200
- # PLE checkpoint shards form one runtime embedding parameter.
201
- shard_prefix = f"{prefix}.shard_"
202
- if any(name.startswith(shard_prefix) for name in ignored_layers):
203
- return Qwen4ExpPLEUnquantizedEmbeddingMethod()
204
- if not quant_config.is_checkpoint_fp8_serialized:
205
- raise NotImplementedError(
206
- "Qwen4Exp PLE embedding only supports serialized FP8 checkpoints"
207
- )
208
- return Qwen4ExpPLEFp8EmbeddingMethod()
209
-
210
- def apply(
211
- self,
212
- layer: nn.Module,
213
- x: torch.Tensor,
214
- bias: torch.Tensor | None = None,
215
- ) -> torch.Tensor:
216
- raise NotImplementedError("PLE weights only support embedding lookup")
217
-
218
- def embedding(self, layer: nn.Module, input_: torch.Tensor) -> torch.Tensor:
219
- return F.embedding(input_, layer.weight)
220
-
221
- @abstractmethod
222
- def dequantize(
223
- self,
224
- layer: nn.Module,
225
- embeddings: torch.Tensor,
226
- output_dtype: torch.dtype,
227
- ) -> torch.Tensor:
228
- """Convert looked-up PLE rows to the activation dtype."""
229
- raise NotImplementedError
230
-
231
-
232
-class Qwen4ExpPLEUnquantizedEmbeddingMethod(Qwen4ExpPLEEmbeddingMethod):
233
- """Unquantized PLE embedding storage and lookup semantics."""
234
-
235
- def create_weights(
236
- self,
237
- layer: Qwen4ExpPLEEmbedding,
238
- input_size_per_partition: int,
239
- output_partition_sizes: list[int],
240
- input_size: int,
241
- output_size: int,
242
- params_dtype: torch.dtype,
243
- **extra_weight_attrs,
244
- ) -> None:
245
- del input_size, output_size
246
- weight = nn.Parameter(
247
- layer.allocate_embedding_weight(
248
- sum(output_partition_sizes),
249
- input_size_per_partition,
250
- params_dtype,
251
- ),
252
- requires_grad=False,
253
- )
254
- set_weight_attrs(weight, {"input_dim": 1, "output_dim": 0})
255
- set_weight_attrs(weight, extra_weight_attrs)
256
- layer.register_parameter("weight", weight)
257
-
258
- def dequantize(
259
- self,
260
- layer: nn.Module,
261
- embeddings: torch.Tensor,
262
- output_dtype: torch.dtype,
263
- ) -> torch.Tensor:
264
- del layer, output_dtype
265
- return embeddings
266
-
267
-
268
-class Qwen4ExpPLEFp8EmbeddingMethod(Qwen4ExpPLEEmbeddingMethod):
269
- """FP8 PLE embedding with one global checkpoint scale."""
270
-
271
- def create_weights(
272
- self,
273
- layer: Qwen4ExpPLEEmbedding,
274
- input_size_per_partition: int,
275
- output_partition_sizes: list[int],
276
- input_size: int,
277
- output_size: int,
278
- params_dtype: torch.dtype,
279
- **extra_weight_attrs,
280
- ) -> None:
281
- del input_size, output_size, params_dtype
282
- weight_loader = extra_weight_attrs.get("weight_loader")
283
- weight = ModelWeightParameter(
284
- data=layer.allocate_embedding_weight(
285
- sum(output_partition_sizes),
286
- input_size_per_partition,
287
- torch.float8_e4m3fn,
288
- ),
289
- input_dim=1,
290
- output_dim=0,
291
- weight_loader=weight_loader,
292
- )
293
- layer.register_parameter("weight", weight)
294
-
295
- weight_scale = create_fp8_scale_parameter(
296
- PerTensorScaleParameter,
297
- output_partition_sizes,
298
- input_size_per_partition,
299
- None,
300
- weight_loader,
301
- scale_dtype=torch.float32,
302
- )
303
- layer.register_parameter("weight_scale", weight_scale)
304
-
305
- def process_weights_after_loading(self, layer: nn.Module) -> None:
306
- """Reject FP8 PLE checkpoints without a global scale."""
307
- sentinel = torch.finfo(torch.float32).min
308
- if torch.any(layer.weight_scale == sentinel):
309
- raise ValueError("FP8 PLE checkpoint is missing its global scale")
310
-
311
- def dequantize(
312
- self,
313
- layer: nn.Module,
314
- embeddings: torch.Tensor,
315
- output_dtype: torch.dtype,
316
- ) -> torch.Tensor:
317
- weight_scale = getattr(layer, "weight_scale", None)
318
- if weight_scale is None:
319
- raise RuntimeError("FP8 PLE embedding is missing its global scale")
320
- if weight_scale.device != embeddings.device:
321
- raise RuntimeError("FP8 PLE embedding scale must be on the output device")
322
- return embeddings.to(output_dtype) * weight_scale.to(output_dtype)
323
-
324
-
325
-class Qwen4ExpPLEDeviceEmbedding(Qwen4ExpPLEEmbedding):
326
- """PLE table allocated on the active model device."""
327
-
328
- def allocate_embedding_weight(
329
- self,
330
- num_embeddings: int,
331
- embedding_dim: int,
332
- dtype: torch.dtype,
333
- ) -> torch.Tensor:
334
- """Allocate the complete PLE weight on the active device."""
335
- return torch.empty(num_embeddings, embedding_dim, dtype=dtype)
336
-
337
- def start_prefetch(
338
- self,
339
- hidden_states: torch.Tensor,
340
- ngram_ids: torch.Tensor,
341
- ) -> None:
342
- """Resident embedding prefetch is a no-op."""
343
- return None
344
-
345
- def forward(self, ngram_ids: torch.Tensor) -> torch.Tensor:
346
- """Gather ETP inputs, look up embeddings, and select local rows."""
347
- slot_size, slot_offset = self._get_dp_gather_slot(ngram_ids.shape[0])
348
- gathered_ids = self._gather_dp_ids(ngram_ids, slot_size)
349
- embeddings = super().forward(gathered_ids)
350
- return self._select_embeddings(
351
- embeddings,
352
- ngram_ids.shape[0],
353
- slot_offset,
354
- )
355
-
356
-
357
-@triton.jit
358
-def _lookup_ple_embedding_from_pinned_kernel(
359
- weight_ptr,
360
- ids_ptr,
361
- output_ptr,
362
- embedding_dim,
363
- tp_vocab_start,
364
- tp_vocab_end,
365
- BLOCK_D: tl.constexpr,
366
-):
367
- """Look up TP-owned PLE rows through a CUDA view of pinned host memory."""
368
- row_id = tl.program_id(0)
369
- global_idx = tl.load(ids_ptr + row_id)
370
- in_range = (global_idx >= tp_vocab_start) & (global_idx < tp_vocab_end)
371
- local_idx = tl.where(in_range, global_idx - tp_vocab_start, 0)
372
- offsets = tl.arange(0, BLOCK_D)
373
- store_mask = offsets < embedding_dim
374
- load_mask = store_mask & in_range
375
- values = tl.load(
376
- weight_ptr + local_idx * embedding_dim + offsets,
377
- mask=load_mask,
378
- other=0.0,
379
- )
380
- tl.store(
381
- output_ptr + row_id * embedding_dim + offsets,
382
- values,
383
- mask=store_mask,
384
- )
385
-
386
-
387
-class Qwen4ExpPLEPinnedHostEmbedding(Qwen4ExpPLEEmbedding):
388
- """PLE table loaded into pinned CPU memory and looked up through UVA."""
389
-
390
- supports_prefetch: ClassVar[bool] = True
391
-
392
- def __init__(
393
- self,
394
- num_embeddings: int,
395
- embedding_dim: int,
396
- *,
397
- params_dtype: torch.dtype,
398
- padding_size: int,
399
- prefix: str,
400
- embedding_method: Qwen4ExpPLEEmbeddingMethod,
401
- num_ngram_heads: int = 1,
402
- max_total_tokens: int = 0,
403
- data_parallel_rank: int = 0,
404
- ) -> None:
405
- if not is_uva_available():
406
- raise RuntimeError("Engram CPU offload requires UVA support")
407
- super().__init__(
408
- num_embeddings,
409
- embedding_dim,
410
- params_dtype=params_dtype,
411
- padding_size=padding_size,
412
- prefix=prefix,
413
- embedding_method=embedding_method,
414
- num_ngram_heads=num_ngram_heads,
415
- max_total_tokens=max_total_tokens,
416
- data_parallel_rank=data_parallel_rank,
417
- )
418
- self._uva_weight = get_accelerator_view_from_cpu_tensor(self.weight)
419
- self._block_d = triton.next_power_of_2(self.embedding_dim)
420
- self._prefetch_stream = torch.cuda.Stream(device=self._uva_weight.device)
421
- self._prefetch_buffer = torch.empty(
422
- max_total_tokens * self.etp_data_parallel_size,
423
- num_ngram_heads,
424
- self.embedding_dim,
425
- dtype=self.weight.dtype,
426
- device=self._uva_weight.device,
427
- )
428
- self._output_dim = num_ngram_heads * self.embedding_dim
429
-
430
- def allocate_embedding_weight(
431
- self,
432
- num_embeddings: int,
433
- embedding_dim: int,
434
- dtype: torch.dtype,
435
- ) -> torch.Tensor:
436
- """Allocate the complete PLE weight directly in pinned CPU memory."""
437
- return torch.empty(
438
- num_embeddings,
439
- embedding_dim,
440
- dtype=dtype,
441
- device="cpu",
442
- pin_memory=True,
443
- )
444
-
445
- def _lookup(
446
- self,
447
- input_ids: torch.Tensor,
448
- output: torch.Tensor | None = None,
449
- ) -> torch.Tensor:
450
- """Look up local ETP rows while preserving the weight storage dtype."""
451
- expected_shape = (*input_ids.shape, self.embedding_dim)
452
- if output is None:
453
- output = torch.empty(
454
- expected_shape,
455
- dtype=self.weight.dtype,
456
- device=input_ids.device,
457
- )
458
- elif (
459
- tuple(output.shape) != expected_shape
460
- or output.dtype != self.weight.dtype
461
- or output.device != input_ids.device
462
- ):
463
- raise ValueError(
464
- "PLE prefetch output must match the input shape, weight dtype, "
465
- "and input device"
466
- )
467
-
468
- flat_ids = input_ids.reshape(-1).long()
469
- if flat_ids.numel():
470
- _lookup_ple_embedding_from_pinned_kernel[(flat_ids.numel(),)](
471
- self._uva_weight,
472
- flat_ids,
473
- output,
474
- self.embedding_dim,
475
- self.shard_indices.org_vocab_start_index,
476
- self.shard_indices.org_vocab_end_index,
477
- BLOCK_D=self._block_d,
478
- )
479
- return output
480
-
481
- def _reduce_etp_embeddings(self, embeddings: torch.Tensor) -> torch.Tensor:
482
- """Combine pinned lookup results owned by different ETP ranks."""
483
- if self.tp_size == 1:
484
- return embeddings
485
- assert self.parallel_group is not None
486
- if embeddings.dtype in (torch.float8_e4m3fn, torch.float8_e5m2):
487
- # Each vocabulary row has one owner, so reduce the raw FP8 bytes.
488
- reduced = self.parallel_group.all_reduce(embeddings.view(torch.int8))
489
- return reduced.view(embeddings.dtype)
490
- return self.parallel_group.all_reduce(embeddings)
491
-
492
- @eager_break_during_capture
493
- def start_prefetch(
494
- self,
495
- hidden_states: torch.Tensor,
496
- ngram_ids: torch.Tensor,
497
- ) -> None:
498
- """Gather ETP IDs and launch their UVA lookup on the side stream."""
499
- slot_size, _ = self._get_dp_gather_slot(ngram_ids.shape[0])
500
- gathered_ids = self._gather_dp_ids(ngram_ids, slot_size)
501
- active_output = self._prefetch_buffer[: gathered_ids.shape[0]]
502
- prefetch_stream = self._prefetch_stream
503
- prefetch_stream.wait_stream(torch.cuda.current_stream())
504
- gathered_ids.record_stream(prefetch_stream)
505
- with torch.cuda.stream(prefetch_stream):
506
- self._lookup(gathered_ids, output=active_output)
507
-
508
- @eager_break_during_capture
509
- def _finalize_prefetch(
510
- self,
511
- prefetch_output: torch.Tensor,
512
- output: torch.Tensor,
513
- ) -> None:
514
- """Join the side stream, reduce ETP shards, and select local rows."""
515
- torch.cuda.current_stream().wait_stream(self._prefetch_stream)
516
- slot_size, slot_offset = self._get_dp_gather_slot(output.shape[0])
517
- active_output = prefetch_output[: slot_size * self.etp_data_parallel_size]
518
- embeddings = self._reduce_etp_embeddings(active_output)
519
- embeddings = self._select_embeddings(
520
- embeddings,
521
- output.shape[0],
522
- slot_offset,
523
- )
524
- output.copy_(embeddings.flatten(-2))
525
-
526
- def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
527
- """Finish the pinned lookup into graph-owned output storage."""
528
- output = self._prefetch_buffer.new_empty(
529
- (hidden_states.shape[0], self._output_dim)
530
- )
531
- self._finalize_prefetch(self._prefetch_buffer, output)
532
- return output
32
+__all__ = [
33
+ "Qwen4ExpPLEDeviceEmbedding",
34
+ "Qwen4ExpPLEEmbedding",
35
+ "Qwen4ExpPLEEmbeddingMethod",
36
+ "Qwen4ExpPLEFp8EmbeddingMethod",
37
+ "Qwen4ExpPLEPinnedHostEmbedding",
38
+ "Qwen4ExpPLEUnquantizedEmbeddingMethod",
39
+ "Qwen4ExpNGramEmbedding",
40
+]
41
42
43
class Qwen4ExpNGramEmbedding(nn.Module):