mirror of
https://github.com/vllm-project/vllm.git
synced 2026-10-28 09:45:26 +08:00
[Model] Add Recursive Flint (#62938)
Signed-off-by: Woosuk Kwon <woosuk@inferact.ai> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
856b98415b
commit
899a1689e2
31 files changed
+7559
-2
No files matched your search
@@ -155,6 +155,17 @@ steps:
|
||||
commands:
|
||||
- pytest -v -s models/inkling/rocm
|
||||
|
||||
- label: ":nvidia: (B200) Flint"
|
||||
key: flint-unit-tests-b200
|
||||
timeout_in_minutes: 30
|
||||
device: b200-k8s
|
||||
source_file_dependencies:
|
||||
- vllm/models/flint/
|
||||
- tests/models/flint/
|
||||
commands:
|
||||
- uv pip install --system 'gguf>=0.19.0'
|
||||
- pytest -v -s models/flint
|
||||
|
||||
- label: ":nvidia: (B200) Kimi K3"
|
||||
key: kimi-k3-unit-tests-b200
|
||||
timeout_in_minutes: 40
|
||||
|
||||
+5
-2
@@ -972,12 +972,15 @@ RUN set -eux; \
|
||||
|
||||
# Install vllm-openai dependencies (saves ~2.6s per build)
|
||||
# These are stable packages that don't depend on vLLM itself
|
||||
# From versions.json: .openai_server_extras.timm, .openai_server_extras.runai_model_streamer
|
||||
# From versions.json: .openai_server_extras.timm, .openai_server_extras.runai_model_streamer,
|
||||
# .openai_server_extras.gguf (NVFP4 GGUF checkpoints, e.g. RecursiveSI/flint-nvfp4-gguf)
|
||||
ARG TIMM_VERSION=">=1.0.17"
|
||||
ARG RUNAI_MODEL_STREAMER_VERSION=">=0.15.7"
|
||||
ARG GGUF_VERSION=">=0.19.0"
|
||||
RUN --mount=type=cache,target=/opt/uv/cache \
|
||||
uv pip install --system accelerate 'modelscope<1.38' \
|
||||
"timm${TIMM_VERSION}" "runai-model-streamer[s3,gcs,azure]${RUNAI_MODEL_STREAMER_VERSION}"
|
||||
"timm${TIMM_VERSION}" "runai-model-streamer[s3,gcs,azure]${RUNAI_MODEL_STREAMER_VERSION}" \
|
||||
"gguf${GGUF_VERSION}"
|
||||
|
||||
# ============================================================
|
||||
# VLLM INSTALLATION (depends on build stage)
|
||||
|
||||
@@ -84,6 +84,9 @@
|
||||
},
|
||||
"RUNAI_MODEL_STREAMER_VERSION": {
|
||||
"default": ">=0.15.7"
|
||||
},
|
||||
"GGUF_VERSION": {
|
||||
"default": ">=0.19.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -400,6 +400,7 @@ th {
|
||||
| `FalconForCausalLM` | Falcon | `tiiuae/falcon-7b`, `tiiuae/falcon-40b`, `tiiuae/falcon-rw-7b`, etc. | | ✅︎ |
|
||||
| `FalconMambaForCausalLM` | FalconMamba | `tiiuae/falcon-mamba-7b`, `tiiuae/falcon-mamba-7b-instruct`, etc. | | ✅︎ |
|
||||
| `FalconH1ForCausalLM` | Falcon-H1 | `tiiuae/Falcon-H1-34B-Base`, `tiiuae/Falcon-H1-34B-Instruct`, etc. | ✅︎ | ✅︎ |
|
||||
| `FlintForCausalLM` | Flint | `RecursiveSI/flint`, `RecursiveSI/flint-nvfp4-gguf` (GGUF) | | ✅︎ |
|
||||
| `GemmaForCausalLM` | Gemma | `google/gemma-2b`, `google/gemma-1.1-2b-it`, etc. | ✅︎ | ✅︎ |
|
||||
| `Gemma2ForCausalLM` | Gemma 2 | `google/gemma-2-9b`, `google/gemma-2-27b`, etc. | ✅︎ | ✅︎ |
|
||||
| `Gemma3ForCausalLM` | Gemma 3 | `google/gemma-3-1b-it`, etc. | ✅︎ | ✅︎ |
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
# yaml-language-server: $schema=https://recipes.vllm.ai/recipe.schema.json
|
||||
|
||||
meta:
|
||||
title: "Flint"
|
||||
slug: "flint"
|
||||
provider: "RecursiveSI"
|
||||
description: "39.8B-parameter (2.5B active) MoE with hybrid sliding-window / full attention, reasoning and tool calling; BF16 and NVFP4 checkpoints on a single Blackwell GPU."
|
||||
date_added: 2026-10-18
|
||||
date_updated: 2026-10-21
|
||||
tasks:
|
||||
- text
|
||||
performance_headline: "Single GB200, NVFP4: ~2.0 ms per output token at batch 1 with 100K context"
|
||||
related_recipes: []
|
||||
default_hardware: gb200
|
||||
hardware:
|
||||
gb200: verified
|
||||
|
||||
model:
|
||||
model_id: "RecursiveSI/flint"
|
||||
min_vllm_version: "nightly"
|
||||
install:
|
||||
pip:
|
||||
note: "Use a nightly wheel. The NVFP4 GGUF checkpoint also needs `pip install 'gguf>=0.19.0'`."
|
||||
docker:
|
||||
note: "Use the vLLM nightly image."
|
||||
architecture: moe
|
||||
parameter_count: "39.8B"
|
||||
active_parameters: "2.5B"
|
||||
context_length: 262144
|
||||
base_args: []
|
||||
base_env: {}
|
||||
rust_frontend: unsupported
|
||||
|
||||
features:
|
||||
tool_calling:
|
||||
description: "Automatic tool choice with the GLM-4.7 XML tool-call parser (Flint's chat template emits <tool_call>/<arg_key>/<arg_value>); tools are dropped from the prompt when tool_choice is none, as in the reference runtime."
|
||||
args: ["--enable-auto-tool-choice", "--tool-call-parser", "glm47", "--exclude-tools-when-tool-choice-none"]
|
||||
reasoning:
|
||||
description: "Return the <think>…</think> block in the reasoning field; the model decides per turn whether to reason."
|
||||
args: ["--reasoning-parser", "glm47"]
|
||||
|
||||
opt_in_features: []
|
||||
|
||||
variants:
|
||||
default:
|
||||
precision: bf16
|
||||
vram_minimum_gb: 96
|
||||
description: "BF16 safetensors (79.6 GB of weights)."
|
||||
nvfp4:
|
||||
model_id: "RecursiveSI/flint-nvfp4-gguf"
|
||||
precision: nvfp4
|
||||
vram_minimum_gb: 29
|
||||
description: "NVFP4 GGUF served weight-only (W4A16); 23.5 GB resident after load (the LM head is dequantized to BF16). Config and tokenizer come from the BF16 repo."
|
||||
extra_args: ["--hf-config-path", "RecursiveSI/flint", "--tokenizer", "RecursiveSI/flint"]
|
||||
|
||||
compatible_strategies:
|
||||
- single_node_tp
|
||||
|
||||
hardware_overrides: {}
|
||||
|
||||
strategy_overrides:
|
||||
single_node_tp:
|
||||
tp: 1
|
||||
|
||||
guide: |
|
||||
## Overview
|
||||
|
||||
Flint is a 39.8B-parameter mixture-of-experts model (256 experts, top-6 sigmoid routing plus a
|
||||
shared expert; ~2.5B parameters active per token) with 49 layers: 35 sliding-window layers
|
||||
(window 513, RoPE) and 14 full-attention layers (no RoPE), GQA 32/4, attention sinks, and a
|
||||
262,144-token context. Two checkpoints are served:
|
||||
|
||||
- [RecursiveSI/flint](https://huggingface.co/RecursiveSI/flint): BF16 safetensors.
|
||||
- [RecursiveSI/flint-nvfp4-gguf](https://huggingface.co/RecursiveSI/flint-nvfp4-gguf): a single
|
||||
NVFP4 GGUF (repacked losslessly at load and run as W4A16, like the reference runtime). It ships
|
||||
no config or tokenizer; both come from the BF16 repo.
|
||||
|
||||
Tested end to end on one GB200 (TP=1, BF16 KV cache, CUDA graphs, no torch.compile) for both
|
||||
checkpoints. On a single GPU the Flint kernels are always used (fused MoE, small-batch CuTe GEMV,
|
||||
CuTe decode attention, low-latency decode path for <= 8 requests); TP > 1 and expert parallelism
|
||||
fall back to stock vLLM layers and are not validated.
|
||||
|
||||
## Launch
|
||||
|
||||
BF16:
|
||||
|
||||
```bash
|
||||
vllm serve RecursiveSI/flint --served-model-name flint \
|
||||
--enable-auto-tool-choice --tool-call-parser glm47 --reasoning-parser glm47 \
|
||||
--exclude-tools-when-tool-choice-none
|
||||
```
|
||||
|
||||
NVFP4 (requires `gguf>=0.19.0`):
|
||||
|
||||
```bash
|
||||
vllm serve RecursiveSI/flint-nvfp4-gguf \
|
||||
--hf-config-path RecursiveSI/flint --tokenizer RecursiveSI/flint \
|
||||
--served-model-name flint \
|
||||
--enable-auto-tool-choice --tool-call-parser glm47 --reasoning-parser glm47 \
|
||||
--exclude-tools-when-tool-choice-none
|
||||
```
|
||||
|
||||
## Client usage
|
||||
|
||||
Use `/v1/chat/completions`. Sampling defaults from `generation_config.json`: temperature 1.0,
|
||||
top_p 0.95, top_k 20 (greedy decoding is not validated). Reasoning is returned in `reasoning`
|
||||
and counts toward `max_tokens`; use `max_tokens` of at least 32768 for hard problems.
|
||||
|
||||
```bash
|
||||
curl localhost:8000/v1/chat/completions -H 'Content-Type: application/json' -d '{
|
||||
"model": "flint",
|
||||
"messages": [{"role": "user", "content": "What is 17*23? Answer briefly."}],
|
||||
"max_tokens": 1024
|
||||
}'
|
||||
```
|
||||
|
||||
Accuracy on GB200 with this recipe (BF16 / NVFP4): GSM8K 0.950 / 0.950, GPQA-Diamond (4 repeats)
|
||||
0.721 / 0.696, MRCR (48 samples, up to 114K-token prompts) ~0.47 / ~0.46, tool-calling suite
|
||||
(19 cases x 10: typed args, tool selection, parallel calls, multi-turn, tool_choice none,
|
||||
escaping, streaming) 10/10 on every case.
|
||||
|
||||
## Benchmarking
|
||||
|
||||
Forward time on one GB200 (`vllm bench forward`, batch x context, us):
|
||||
|
||||
| shape | BF16 | NVFP4 |
|
||||
|---|---|---|
|
||||
| decode 1 x 100K | 2341 | 1783 |
|
||||
| decode 8 x 100K | 6836 | 5703 |
|
||||
| decode 32 x 100K | 19580 | 16716 |
|
||||
| prefill 8K @ 100K | 145617 | 144765 |
|
||||
|
||||
Serving (NVFP4, `vllm bench serve`, random 1000 in / 1000 out): TPOT 2.0 ms at concurrency 1,
|
||||
2.8 ms at 8, 4.9 ms at 32, 8.5 ms at 128; median inter-token latency 2.06 ms at batch 1 with
|
||||
100K context. Prefill runs eagerly and is CPU-bound on kernel-launch overhead: TTFT is ~60 ms
|
||||
for a short prompt and ~125 ms for an 8K-token prompt.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- Rust frontend: not supported for this recipe. It rejects `--exclude-tools-when-tool-choice-none`,
|
||||
and for NVFP4 also `--hf-config-path` / `--tokenizer`; keep the default Python frontend.
|
||||
- Reasoning parser: the reference runtime's `deepseek-v3` name maps to vLLM's `deepseek_v3`
|
||||
parser, which only extracts reasoning when the request enables `thinking`; with Flint the
|
||||
reasoning then stays in `content`. Use `glm47`.
|
||||
- If `content` contains literal `<think>` or `<tool_call>` text, the server was started without
|
||||
the parsers above.
|
||||
- Batch-1 decode time varies by ~3-4% between sessions (a GPU-side state, not configuration).
|
||||
|
||||
## References
|
||||
|
||||
- [Flint (BF16) model card](https://huggingface.co/RecursiveSI/flint)
|
||||
- [Flint NVFP4 model card](https://huggingface.co/RecursiveSI/flint-nvfp4-gguf)
|
||||
@@ -0,0 +1,278 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""The Flint split-KV decode attention kernel must match an fp64 reference at
|
||||
bf16 output-rounding level, mirror FlashInfer trtllm-gen's numerics, and stay
|
||||
correct under CUDA graph replay with new sequence lengths."""
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from vllm.platforms import current_platform
|
||||
|
||||
if not current_platform.is_cuda() or not current_platform.is_device_capability_family(
|
||||
100
|
||||
):
|
||||
pytest.skip("Flint decode attention targets SM100", allow_module_level=True)
|
||||
|
||||
from flashinfer.decode import trtllm_batch_decode_with_kv_cache # noqa: E402
|
||||
|
||||
from vllm.models.flint.ops.decode_attention import flint_decode_attention # noqa: E402
|
||||
|
||||
HQ, HKV, D, PAGE = 32, 4, 128, 16
|
||||
SCALE = D**-0.5
|
||||
WINDOW = 513
|
||||
|
||||
|
||||
def _make(
|
||||
seq_lens: list[int], seed: int = 0
|
||||
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
|
||||
"""Random q/K/V/sinks; blocks are shuffled so block tables are non-trivial."""
|
||||
g = torch.Generator(device="cuda").manual_seed(seed)
|
||||
pages = [(s + PAGE - 1) // PAGE for s in seq_lens]
|
||||
num_blocks = sum(pages) + 5
|
||||
kv = torch.randn(num_blocks, HKV, PAGE, 2 * D, device="cuda", generator=g)
|
||||
perm = torch.randperm(num_blocks, device="cuda", generator=g).int()
|
||||
bt = torch.zeros(len(seq_lens), max(pages + [1]) + 2, dtype=torch.int32)
|
||||
bt = bt.cuda()
|
||||
o = 0
|
||||
for i, n in enumerate(pages):
|
||||
bt[i, :n] = perm[o : o + n]
|
||||
o += n
|
||||
q = torch.randn(len(seq_lens), HQ, D, device="cuda", generator=g) * 2
|
||||
sinks = torch.randn(HQ, device="cuda", generator=g) * 2
|
||||
sl = torch.tensor(seq_lens, dtype=torch.int32, device="cuda")
|
||||
return q.bfloat16(), kv.bfloat16(), bt, sl, sinks
|
||||
|
||||
|
||||
def _reference(q, kv, bt, seq_lens, sinks, window):
|
||||
out = torch.zeros(q.shape, dtype=torch.float64, device=q.device)
|
||||
for i, s in enumerate(seq_lens.tolist()):
|
||||
if s == 0:
|
||||
continue
|
||||
n = (s + PAGE - 1) // PAGE
|
||||
x = kv[bt[i, :n].long()].permute(1, 0, 2, 3).reshape(HKV, n * PAGE, 2 * D)
|
||||
x = x[:, max(s - window, 0) if window else 0 : s].double()
|
||||
k, v = x[..., :D], x[..., D:]
|
||||
scores = torch.einsum("hgd,htd->hgt", q[i].double().view(HKV, -1, D), k)
|
||||
scores = scores * SCALE
|
||||
sink = sinks.double().view(HKV, -1, 1)
|
||||
m = torch.maximum(scores.amax(-1, keepdim=True), sink)
|
||||
p = torch.exp(scores - m)
|
||||
denom = p.sum(-1, keepdim=True) + torch.exp(sink - m)
|
||||
out[i] = (torch.einsum("hgt,htd->hgd", p, v) / denom).reshape(HQ, D)
|
||||
return out
|
||||
|
||||
|
||||
def _flashinfer(q, kv, bt, seq_lens, sinks, window):
|
||||
workspace = torch.zeros(128 << 20, dtype=torch.uint8, device="cuda")
|
||||
k_cache, v_cache = kv.split(D, dim=-1)
|
||||
return trtllm_batch_decode_with_kv_cache(
|
||||
q,
|
||||
(k_cache, v_cache),
|
||||
workspace,
|
||||
bt,
|
||||
seq_lens,
|
||||
int(seq_lens.max()),
|
||||
bmm1_scale=SCALE,
|
||||
window_left=window - 1 if window else -1,
|
||||
sinks=sinks,
|
||||
kv_layout="HND",
|
||||
backend="trtllm-gen",
|
||||
)
|
||||
|
||||
|
||||
def _flint(q, kv, bt, seq_lens, sinks, window, out=None):
|
||||
out = torch.empty_like(q) if out is None else out
|
||||
return flint_decode_attention(q, kv, bt, seq_lens, sinks, window, SCALE, out)
|
||||
|
||||
|
||||
def _rel_err(x, ref):
|
||||
"""|x - ref| relative to each output row's max magnitude."""
|
||||
scale = ref.abs().amax(-1, keepdim=True).clamp_min(1e-6)
|
||||
return (x.double() - ref).abs() / scale
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"seq_lens",
|
||||
[
|
||||
[1],
|
||||
[17],
|
||||
[1000],
|
||||
[20011],
|
||||
[5, 77, 1000, 4097, 16, 33, 513, 3001],
|
||||
[100, 2000, 37] * 10 + [1, 9000],
|
||||
],
|
||||
ids=["1", "17", "1000", "20011", "mixed8", "mixed32"],
|
||||
)
|
||||
@pytest.mark.parametrize("window", [None, WINDOW], ids=["full", "swa"])
|
||||
@torch.inference_mode()
|
||||
def test_matches_reference_and_flashinfer(seq_lens: list[int], window: int | None):
|
||||
args = _make(seq_lens)
|
||||
ref = _reference(*args, window)
|
||||
out = _flint(*args, window)
|
||||
fi_err = _rel_err(_flashinfer(*args, window), ref)
|
||||
err = _rel_err(out, ref)
|
||||
# One bf16 ulp of the row max is 2^-8; trtllm-gen also rounds its split
|
||||
# partials to bf16, so its error bounds ours.
|
||||
assert err.max() < 8e-3
|
||||
assert err.max() <= fi_err.max() + 1e-3
|
||||
assert err.mean() <= fi_err.mean() + 1e-4
|
||||
|
||||
|
||||
@pytest.mark.parametrize("seq_len", [513, 514, 529, 530, 545, 20000])
|
||||
@torch.inference_mode()
|
||||
def test_window_crosses_block_boundary(seq_len: int):
|
||||
"""The first attended page is partial; earlier tokens must be masked."""
|
||||
args = _make([seq_len, seq_len + 7], seed=seq_len)
|
||||
err = _rel_err(_flint(*args, WINDOW), _reference(*args, WINDOW))
|
||||
assert err.max() < 8e-3
|
||||
|
||||
|
||||
@torch.inference_mode()
|
||||
def test_launch_from_another_stream_waits_for_the_previous_one():
|
||||
"""Launches share the split workspace and arrival counters, so a launch
|
||||
from a different stream must not start before the previous one ends."""
|
||||
args = _make([20011] * 4)
|
||||
_flint(*args, None)
|
||||
stream_a, stream_b = torch.cuda.Stream(), torch.cuda.Stream()
|
||||
torch.accelerator.synchronize()
|
||||
with torch.cuda.stream(stream_a):
|
||||
torch.cuda._sleep(100_000_000) # ~50 ms at ~2 GHz
|
||||
a_done = torch.cuda.Event()
|
||||
_flint(*args, None)
|
||||
a_done.record()
|
||||
with torch.cuda.stream(stream_b):
|
||||
_flint(*args, None)
|
||||
b_done = torch.cuda.Event()
|
||||
b_done.record()
|
||||
b_done.synchronize()
|
||||
assert a_done.query()
|
||||
|
||||
|
||||
@torch.inference_mode()
|
||||
def test_empty_request_writes_zeros():
|
||||
q, kv, bt, sl, sinks = _make([0, 40])
|
||||
out = torch.full_like(q, float("nan"))
|
||||
_flint(q, kv, bt, sl, sinks, None, out)
|
||||
assert (out[0] == 0).all()
|
||||
assert _rel_err(out[1:], _reference(q, kv, bt, sl, sinks, None)[1:]).max() < 8e-3
|
||||
|
||||
|
||||
@torch.inference_mode()
|
||||
def test_probabilities_summed_in_fp32():
|
||||
"""Like trtllm-gen, P is rounded to bf16 for P @ V but the softmax
|
||||
denominator sums the fp32 probabilities. With V = 1, one max-logit token
|
||||
per page (so every split sees the same running max) and all other
|
||||
probabilities just below a bf16 rounding midpoint, the output is visibly
|
||||
below 1 (it would be exactly 1 with fp32 P or a bf16-P denominator)."""
|
||||
n = 1000
|
||||
q, kv, bt, sl, sinks = _make([n])
|
||||
kv.zero_()
|
||||
q.zero_()
|
||||
q[..., 0] = 1.0
|
||||
q[..., 1] = 2**-6
|
||||
flat = kv.view(-1, HKV, PAGE, 2 * D)[bt[0, : (n + PAGE - 1) // PAGE].long()]
|
||||
flat[..., 0] = -7.8125 # p = exp2((s - 0) * scale * log2e) = 0.50195..
|
||||
flat[..., 1] = 0.9296875
|
||||
flat[:, :, 0, :2] = 0.0 # first token of every page at the max (p = 1)
|
||||
flat[..., D:] = 1.0
|
||||
kv.view(-1, HKV, PAGE, 2 * D)[bt[0, : (n + PAGE - 1) // PAGE].long()] = flat
|
||||
sinks.fill_(-100.0)
|
||||
out = _flint(q, kv, bt, sl, sinks, None)
|
||||
expected = _flashinfer(q, kv, bt, sl, sinks, None)
|
||||
assert (expected == 0.99609375).all()
|
||||
torch.testing.assert_close(out, expected, atol=0, rtol=0)
|
||||
|
||||
|
||||
@torch.inference_mode()
|
||||
def test_cuda_graph_replay_with_new_lengths():
|
||||
"""Grid shape depends only on the batch size; the split-combine counters
|
||||
reset themselves, so replays with different lengths stay correct."""
|
||||
lens_a = [30000, 17, 4100, 515]
|
||||
lens_b = [1, 31000, 513, 9000]
|
||||
q, kv, bt, sl, sinks = _make(lens_b)
|
||||
out = torch.empty_like(q)
|
||||
_, _, bt_a, sl_a, _ = _make(lens_a)
|
||||
bt_a = torch.nn.functional.pad(bt_a, (0, bt.shape[1] - bt_a.shape[1]))
|
||||
static_bt, static_sl = bt_a.clone(), sl_a.clone()
|
||||
for window in (None, WINDOW):
|
||||
_flint(q, kv, static_bt, static_sl, sinks, window, out) # warm up / compile
|
||||
graph = torch.cuda.CUDAGraph()
|
||||
with torch.cuda.graph(graph):
|
||||
_flint(q, kv, static_bt, static_sl, sinks, window, out)
|
||||
for bt_x, sl_x in ((bt, sl), (bt_a, sl_a), (bt, sl)):
|
||||
static_bt.copy_(bt_x)
|
||||
static_sl.copy_(sl_x)
|
||||
graph.replay()
|
||||
ref = _reference(q, kv, static_bt, static_sl, sinks, window)
|
||||
assert _rel_err(out, ref).max() < 8e-3
|
||||
|
||||
|
||||
def _early_trigger_writer():
|
||||
"""A PDL kernel that releases its dependents immediately, idles ~2 us,
|
||||
then writes one token's K/V into the cache, like
|
||||
``qk_norm_rope_cache(launch_dependents=True)``."""
|
||||
import cutlass
|
||||
import cutlass.cute as cute
|
||||
from cutlass import Int32, Int64
|
||||
|
||||
class Writer:
|
||||
@cute.jit
|
||||
def __call__(self, src, dst, cycles: Int32, stream):
|
||||
self.kernel(src, dst, cycles).launch(
|
||||
grid=(1, 1, 1), block=(256, 1, 1), stream=stream, use_pdl=True
|
||||
)
|
||||
|
||||
@cute.kernel
|
||||
def kernel(self, src, dst, cycles: Int32):
|
||||
cute.arch.griddepcontrol_wait()
|
||||
cute.arch.griddepcontrol_launch_dependents()
|
||||
t0 = cute.arch.clock64()
|
||||
while cute.arch.clock64() - t0 < Int64(cycles):
|
||||
pass
|
||||
tid, _, _ = cute.arch.thread_idx()
|
||||
dst[tid] = src[tid]
|
||||
|
||||
from cutlass.cute.runtime import make_fake_stream
|
||||
from quack.compile_utils import make_fake_tensor
|
||||
|
||||
bf16 = cutlass.BFloat16
|
||||
return cute.compile(
|
||||
Writer(),
|
||||
make_fake_tensor(bf16, (256,), divisibility=8),
|
||||
make_fake_tensor(bf16, (256,), divisibility=8),
|
||||
Int32(1),
|
||||
make_fake_stream(use_tvm_ffi_env_stream=True),
|
||||
options="--enable-tvm-ffi",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("window", [None, WINDOW], ids=["full", "swa"])
|
||||
@torch.inference_mode()
|
||||
def test_current_token_written_by_pdl_predecessor(window: int | None):
|
||||
"""K/V pages are prefetched before the grid-dependency wait, except the
|
||||
page holding the current token: K/V written there by an early-triggering
|
||||
predecessor must be seen."""
|
||||
writer = _early_trigger_writer()
|
||||
# The last write (request 3, head 3) races the attention kernel; with one
|
||||
# token, a stale read would change that output entirely.
|
||||
lens = [20011, 4096, 17, 1]
|
||||
q, kv, bt, sl, sinks = _make(lens)
|
||||
new_kv = torch.randn(len(lens), HKV, 2 * D, device="cuda").bfloat16() * 4
|
||||
out = torch.empty_like(q)
|
||||
blks = [int(bt[i, (s - 1) // PAGE]) for i, s in enumerate(lens)]
|
||||
_flint(q, kv, bt, sl, sinks, window, out) # compile / workspace
|
||||
graph = torch.cuda.CUDAGraph()
|
||||
with torch.cuda.graph(graph):
|
||||
for i, s in enumerate(lens):
|
||||
for h in range(HKV):
|
||||
writer(new_kv[i, h], kv[blks[i], h, (s - 1) % PAGE], 4000)
|
||||
_flint(q, kv, bt, sl, sinks, window, out)
|
||||
kv_before = kv.clone()
|
||||
graph.replay()
|
||||
torch.accelerator.synchronize()
|
||||
for i, s in enumerate(lens):
|
||||
kv_before[blks[i], :, (s - 1) % PAGE] = new_kv[i]
|
||||
assert torch.equal(kv, kv_before)
|
||||
ref = _reference(q, kv, bt, sl, sinks, window)
|
||||
assert _rel_err(out, ref).max() < 8e-3
|
||||
@@ -0,0 +1,436 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Flint's fused NVFP4 W4A16 MoE (vllm/models/flint/ops/moe.py) must follow the
|
||||
reference rounding rule on both the decode (GEMV) and the grouped-GEMM paths
|
||||
and route exactly like sigmoid top-k."""
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from vllm.models.flint.gguf_nvfp4 import dequantize_nvfp4
|
||||
from vllm.models.flint.ops import moe
|
||||
from vllm.platforms import current_platform
|
||||
|
||||
E, H, INTER, TOPK = 256, 2048, 512, 6
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not current_platform.is_device_capability_family(100),
|
||||
reason="SM100 only",
|
||||
)
|
||||
|
||||
|
||||
def _nvfp4(rows: int, cols: int, gen: torch.Generator):
|
||||
w = torch.randint(
|
||||
0, 256, (E, rows, cols // 2), dtype=torch.uint8, device="cuda", generator=gen
|
||||
)
|
||||
# E4M3 scales over ~2^-6..2^2 with all mantissas.
|
||||
exp = torch.randint(1, 10, (E, rows, cols // 16), device="cuda", generator=gen)
|
||||
man = torch.randint(0, 8, (E, rows, cols // 16), device="cuda", generator=gen)
|
||||
s = ((exp << 3) | man).to(torch.uint8).view(torch.float8_e4m3fn)
|
||||
gs = torch.rand(E, device="cuda", generator=gen) * 2e-3 + 1e-3
|
||||
return w, s, gs
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def weights():
|
||||
gen = torch.Generator(device="cuda").manual_seed(0)
|
||||
w13, s13, gs13 = _nvfp4(2 * INTER, H, gen)
|
||||
w2, s2, gs2 = _nvfp4(H, INTER, gen)
|
||||
router = (torch.randn(E, H, device="cuda", generator=gen) * 0.02).bfloat16()
|
||||
return dict(w13=w13, s13=s13, gs13=gs13, w2=w2, s2=s2, gs2=gs2, router=router)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def flint_weights(weights):
|
||||
return moe.FlintMoEWeights.from_modelopt(
|
||||
weights["w13"],
|
||||
weights["s13"],
|
||||
weights["gs13"],
|
||||
weights["w2"],
|
||||
weights["s2"],
|
||||
weights["gs2"],
|
||||
)
|
||||
|
||||
|
||||
def _inputs(num_tokens: int, seed: int | None = None):
|
||||
gen = torch.Generator(device="cuda").manual_seed(
|
||||
num_tokens if seed is None else seed
|
||||
)
|
||||
x = torch.randn(num_tokens, H, device="cuda", generator=gen)
|
||||
x = (x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True))).bfloat16()
|
||||
shared = (torch.randn(num_tokens, H, device="cuda", generator=gen) * 0.1).bfloat16()
|
||||
return x, shared
|
||||
|
||||
|
||||
def _reference(x, w, ids, tw, shared, bf16_points: bool):
|
||||
"""MoE on exactly dequantized weights with fp32 GEMMs.
|
||||
|
||||
bf16_points=False: everything in fp32 (returns fp32).
|
||||
bf16_points=True: the reference (TRTLLM MoE) rounding rule (returns bf16):
|
||||
fp32 gate/up, act = bf16(silu(g) * u), y_e = bf16(acc_e * global_scale_e),
|
||||
routed = bf16(sum_e tw_e * y_e) in fp32 (tw already bf16-rounded by the
|
||||
router), out = bf16(routed + shared).
|
||||
"""
|
||||
bf = torch.bfloat16
|
||||
routed = torch.zeros(x.shape[0], H, device=x.device)
|
||||
for e in ids.unique().tolist():
|
||||
tok, slot = (ids == e).nonzero(as_tuple=True)
|
||||
w13 = dequantize_nvfp4(w["w13"][e], w["s13"][e], torch.tensor(1.0))
|
||||
w2 = dequantize_nvfp4(w["w2"][e], w["s2"][e], torch.tensor(1.0))
|
||||
gu = (x[tok].float() @ w13.T) * w["gs13"][e]
|
||||
g, u = gu[:, :INTER], gu[:, INTER:]
|
||||
if bf16_points:
|
||||
h = (torch.nn.functional.silu(g) * u).to(bf).float()
|
||||
y = ((h @ w2.T) * w["gs2"][e]).to(bf).float()
|
||||
else:
|
||||
h = torch.nn.functional.silu(g) * u
|
||||
y = (h @ w2.T) * w["gs2"][e]
|
||||
routed.index_add_(0, tok, tw[tok, slot, None] * y)
|
||||
if not bf16_points:
|
||||
return routed + shared.float()
|
||||
return (routed.to(bf).float() + shared.float()).to(bf)
|
||||
|
||||
|
||||
def _rel(a: torch.Tensor, b: torch.Tensor) -> float:
|
||||
return float((a.float() - b.float()).norm() / b.float().norm())
|
||||
|
||||
|
||||
def _run(x, router, fw, shared, decode_max: int | None = None):
|
||||
"""flint_moe with an optionally forced decode/grouped dispatch."""
|
||||
old = moe.DECODE_MAX_TOKENS
|
||||
if decode_max is not None:
|
||||
moe.DECODE_MAX_TOKENS = decode_max
|
||||
try:
|
||||
counter = moe.make_route_counter("cuda")
|
||||
return moe.flint_moe(x, router, fw, TOPK, counter, shared)
|
||||
finally:
|
||||
moe.DECODE_MAX_TOKENS = old
|
||||
|
||||
|
||||
@torch.inference_mode()
|
||||
def test_dequant_exact():
|
||||
"""The E2M1 * E4M3 decode is exact for every (code, scale) pair."""
|
||||
import triton
|
||||
import triton.language as tl
|
||||
|
||||
@triton.jit
|
||||
def kernel(w_ptr, s_ptr, out_ptr, N: tl.constexpr):
|
||||
offs = tl.arange(0, N)
|
||||
o = moe._dequant8(tl.load(w_ptr + offs), tl.load(s_ptr + offs))
|
||||
for j in tl.static_range(8):
|
||||
tl.store(out_ptr + offs * 8 + j, o[j])
|
||||
|
||||
# Every non-NaN E4M3 scale applied to one 16-code block holding codes
|
||||
# 0..15 (two int32 words).
|
||||
scales = torch.arange(256, dtype=torch.uint8, device="cuda")
|
||||
scales = scales[(scales & 0x7F) != 0x7F]
|
||||
codes = torch.arange(16, dtype=torch.uint8, device="cuda")
|
||||
block = codes[0::2] | (codes[1::2] << 4)
|
||||
words = block.repeat(scales.numel()).view(torch.int32)
|
||||
n = words.numel()
|
||||
padded_w = torch.zeros(512, dtype=torch.int32, device="cuda")
|
||||
padded_s = torch.zeros(512, dtype=torch.uint8, device="cuda")
|
||||
padded_w[:n] = words
|
||||
padded_s[:n] = scales.repeat_interleave(2)
|
||||
out_dtype = torch.bfloat16 if moe._DEQUANT_BF16 else torch.float16
|
||||
padded_out = torch.empty(512 * 8, dtype=out_dtype, device="cuda")
|
||||
kernel[(1,)](padded_w, padded_s, padded_out, N=512)
|
||||
ref = dequantize_nvfp4(
|
||||
block.repeat(scales.numel()).view(-1, 8),
|
||||
scales.view(torch.float8_e4m3fn).view(-1, 1),
|
||||
torch.tensor(1.0),
|
||||
)
|
||||
torch.testing.assert_close(
|
||||
padded_out[: n * 8].float(), ref.flatten(), rtol=0, atol=0
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"num_tokens",
|
||||
[1, 5, moe.ROUTE_GEMV_MAX_TOKENS, moe.ROUTE_GEMV_MAX_TOKENS + 1, 300],
|
||||
)
|
||||
@torch.inference_mode()
|
||||
def test_routing(weights, num_tokens: int):
|
||||
x, _ = _inputs(num_tokens)
|
||||
counter = moe.make_route_counter("cuda")
|
||||
ids, tw = moe.route(x, weights["router"], TOPK, counter)
|
||||
if num_tokens > moe.ROUTE_GEMV_MAX_TOKENS:
|
||||
# Same cuBLAS bf16 GEMM as the kernel path; only top-k is under test.
|
||||
logits = (x @ weights["router"].T).float()
|
||||
else:
|
||||
logits = (x.float() @ weights["router"].float().T).bfloat16().float()
|
||||
ref_s, ref_ids = torch.sigmoid(logits).topk(TOPK, dim=-1)
|
||||
torch.testing.assert_close(
|
||||
ids.sort(-1).values, ref_ids.int().sort(-1).values, rtol=0, atol=0
|
||||
)
|
||||
# Renormalized as the reference does, then rounded to bf16.
|
||||
ref_tw = ref_s / (ref_s.sum(-1, keepdim=True) + 1e-20)
|
||||
ref_tw = ref_tw.bfloat16().float()
|
||||
order, ref_order = ids.argsort(-1), ref_ids.argsort(-1)
|
||||
got, ref_tw = tw.gather(1, order), ref_tw.gather(1, ref_order)
|
||||
# The fp32 sigmoid / sum may differ in the last bit: rare 1-ulp bf16 flips.
|
||||
assert (got == ref_tw).float().mean() > 0.99
|
||||
torch.testing.assert_close(got, ref_tw, rtol=2**-7, atol=0)
|
||||
|
||||
|
||||
@torch.inference_mode()
|
||||
def test_topk_ties_pick_lowest_expert():
|
||||
logits = torch.zeros(2, E, device="cuda", dtype=torch.bfloat16)
|
||||
logits[0, [7, 3, 200, 9, 100, 50, 2]] = 1.0
|
||||
logits[1, :] = -1.0
|
||||
ids = torch.empty(2, TOPK, dtype=torch.int32, device="cuda")
|
||||
tw = torch.empty(2, TOPK, dtype=torch.float32, device="cuda")
|
||||
moe._topk_kernel[(2,)](logits, ids, tw, E=E, TOPK=TOPK)
|
||||
assert ids[0].tolist() == [2, 3, 7, 9, 50, 100]
|
||||
assert ids[1].tolist() == [0, 1, 2, 3, 4, 5]
|
||||
torch.testing.assert_close(
|
||||
tw, torch.full_like(tw, 1 / TOPK).bfloat16().float(), rtol=0, atol=0
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"num_tokens",
|
||||
[
|
||||
1,
|
||||
moe.DECODE_MAX_TOKENS - 1,
|
||||
moe.DECODE_MAX_TOKENS,
|
||||
moe.DECODE_MAX_TOKENS + 1,
|
||||
64,
|
||||
600,
|
||||
],
|
||||
)
|
||||
@torch.inference_mode()
|
||||
def test_moe_matches_reference(weights, flint_weights, num_tokens: int):
|
||||
"""Output follows the bf16 rounding rule (bitwise on >= 99% of elements)
|
||||
and stays at the bf16 rounding level of the fp32 reference."""
|
||||
x, shared = _inputs(num_tokens)
|
||||
out = _run(x, weights["router"], flint_weights, shared)
|
||||
ids, tw = moe.route(x, weights["router"], TOPK, moe.make_route_counter("cuda"))
|
||||
ref16 = _reference(x, weights, ids.long(), tw, shared, bf16_points=True)
|
||||
ref32 = _reference(x, weights, ids.long(), tw, shared, bf16_points=False)
|
||||
match = (out == ref16).float().mean().item()
|
||||
assert match >= 0.99, match
|
||||
assert _rel(out, ref32) < 1.25 * _rel(ref16, ref32)
|
||||
tok_err = (out.float() - ref32).norm(dim=-1) / ref32.norm(dim=-1)
|
||||
assert tok_err.max() < 0.01
|
||||
|
||||
|
||||
@torch.inference_mode()
|
||||
def test_experts_module_loads_per_expert_tensors(weights, flint_weights):
|
||||
"""FlintNvfp4Experts assembles per-expert checkpoint tensors correctly and
|
||||
repacks only once every distinct tensor is loaded."""
|
||||
layer = moe.FlintNvfp4Experts(E, H, INTER, TOPK).cuda()
|
||||
|
||||
def ckpt(experts):
|
||||
for e in experts:
|
||||
for proj, rows in (
|
||||
("gate_proj", slice(INTER)),
|
||||
("up_proj", slice(INTER, 2 * INTER)),
|
||||
):
|
||||
yield f"{e}.{proj}.weight", weights["w13"][e, rows]
|
||||
yield f"{e}.{proj}.weight_scale", weights["s13"][e, rows]
|
||||
yield f"{e}.{proj}.weight_scale_2", weights["gs13"][e]
|
||||
yield f"{e}.down_proj.weight", weights["w2"][e]
|
||||
yield f"{e}.down_proj.weight_scale", weights["s2"][e]
|
||||
yield f"{e}.down_proj.weight_scale_2", weights["gs2"][e]
|
||||
|
||||
# Duplicates of expert 0 must not count towards completion.
|
||||
layer.load_weights(ckpt([0] * E))
|
||||
assert layer.weights is None
|
||||
layer.load_weights(ckpt(range(E)))
|
||||
assert layer.weights is not None and not list(layer.parameters())
|
||||
x, shared = _inputs(4)
|
||||
torch.testing.assert_close(
|
||||
layer(x, weights["router"], shared),
|
||||
_run(x, weights["router"], flint_weights, shared),
|
||||
rtol=0,
|
||||
atol=0,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("num_tokens", [1, 64])
|
||||
@torch.inference_mode()
|
||||
def test_shared_expert_on_aux_stream(weights, flint_weights, num_tokens: int):
|
||||
"""Passing the shared-expert module (run on the auxiliary stream, joined
|
||||
by an event) gives the same output as passing its precomputed output."""
|
||||
layer = moe.FlintNvfp4Experts(E, H, INTER, TOPK).cuda()
|
||||
layer.weights = flint_weights
|
||||
for name in list(layer._parameters):
|
||||
delattr(layer, name)
|
||||
shared_mod = torch.nn.Linear(H, H, bias=False, device="cuda", dtype=torch.bfloat16)
|
||||
x, _ = _inputs(num_tokens)
|
||||
expected = layer(x, weights["router"], shared_mod(x))
|
||||
torch.testing.assert_close(
|
||||
layer(x, weights["router"], shared_mod), expected, rtol=0, atol=0
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def bf16_weights():
|
||||
"""BF16 experts as stored by vLLM's FlashInfer TRTLLM BF16 MoE backend,
|
||||
plus the checkpoint-order originals."""
|
||||
import vllm.model_executor.layers.fused_moe # noqa: F401 (import order)
|
||||
from vllm.model_executor.layers.quantization.utils.flashinfer_utils import (
|
||||
convert_moe_weights_to_flashinfer_trtllm_block_layout,
|
||||
)
|
||||
|
||||
gen = torch.Generator(device="cuda").manual_seed(1)
|
||||
w13 = (torch.randn(E, 2 * INTER, H, device="cuda", generator=gen) * 0.03).bfloat16()
|
||||
w2 = (torch.randn(E, H, INTER, device="cuda", generator=gen) * 0.03).bfloat16()
|
||||
ref = (w13.clone(), w2.clone())
|
||||
convert_moe_weights_to_flashinfer_trtllm_block_layout(
|
||||
{}, w13, w2, is_gated_act_gemm=True
|
||||
)
|
||||
return w13, w2, ref
|
||||
|
||||
|
||||
def _trtllm_bf16(x, ids, tw, w13, w2):
|
||||
import flashinfer
|
||||
from flashinfer.fused_moe import WeightLayout
|
||||
|
||||
from vllm.model_executor.layers.fused_moe.experts.trtllm_bf16_moe import (
|
||||
view_as_block_major_k,
|
||||
)
|
||||
|
||||
out = flashinfer.fused_moe.trtllm_bf16_routed_moe(
|
||||
topk_ids=(ids, tw),
|
||||
hidden_states=x,
|
||||
gemm1_weights=view_as_block_major_k(w13),
|
||||
gemm2_weights=view_as_block_major_k(w2),
|
||||
num_experts=E,
|
||||
top_k=TOPK,
|
||||
n_group=None,
|
||||
topk_group=None,
|
||||
intermediate_size=INTER,
|
||||
local_expert_offset=0,
|
||||
local_num_experts=E,
|
||||
routed_scaling_factor=None,
|
||||
routing_method_type=1,
|
||||
use_shuffled_weight=True,
|
||||
weight_layout=WeightLayout.BlockMajorK,
|
||||
do_finalize=True,
|
||||
activation_type=3, # SwiGLU
|
||||
)
|
||||
return out[0] if isinstance(out, list) else out
|
||||
|
||||
|
||||
@pytest.mark.parametrize("num_tokens", [1, moe.BF16_DECODE_MAX_TOKENS])
|
||||
@torch.inference_mode()
|
||||
def test_bf16_decode_matches_trtllm(weights, bf16_weights, num_tokens: int):
|
||||
"""The BF16 decode path reads the TRTLLM layout correctly and follows the
|
||||
TRTLLM BF16 MoE rounding points: on identical routing it agrees with the
|
||||
TRTLLM kernels used for larger batches up to GEMM accumulation order, and
|
||||
with an fp32-GEMM reference of the same rule."""
|
||||
w13, w2, (w13_ref, w2_ref) = bf16_weights
|
||||
maps = moe.trtllm_bf16_row_maps(H, INTER, "cuda")
|
||||
x, shared = _inputs(num_tokens)
|
||||
counter = moe.make_route_counter("cuda")
|
||||
out = moe.flint_moe_bf16_decode(
|
||||
x, weights["router"], w13, w2, maps, TOPK, counter, shared
|
||||
)
|
||||
ids, tw = moe.route(x, weights["router"], TOPK, counter)
|
||||
bf = torch.bfloat16
|
||||
trt = (_trtllm_bf16(x, ids, tw, w13, w2).float() + shared.float()).to(bf)
|
||||
routed = torch.zeros(num_tokens, H, device="cuda")
|
||||
for e in ids.unique().tolist():
|
||||
tok, slot = (ids == e).nonzero(as_tuple=True)
|
||||
gu = x[tok].float() @ w13_ref[e].float().T
|
||||
g, u = gu[:, :INTER], gu[:, INTER:]
|
||||
h = (torch.nn.functional.silu(g) * u).to(bf).float()
|
||||
y = (h @ w2_ref[e].float().T).to(bf).float()
|
||||
routed.index_add_(0, tok.long(), tw[tok, slot, None] * y)
|
||||
ref = (routed.to(bf).float() + shared.float()).to(bf)
|
||||
for other in (trt, ref):
|
||||
match = (out == other).float().mean().item()
|
||||
assert match >= 0.97, match
|
||||
assert _rel(out, other) < 1e-3, _rel(out, other)
|
||||
|
||||
|
||||
def _shared_nvfp4(gen):
|
||||
from vllm.model_executor.layers.quantization.utils.nvfp4_utils import (
|
||||
swizzle_blockscale,
|
||||
)
|
||||
|
||||
out = {}
|
||||
for name, (rows, cols) in (("gu", (2 * 1024, H)), ("down", (H, 1024))):
|
||||
w = torch.randint(
|
||||
0, 256, (rows, cols // 2), dtype=torch.uint8, device="cuda", generator=gen
|
||||
)
|
||||
exp = torch.randint(4, 10, (rows, cols // 16), device="cuda", generator=gen)
|
||||
s = (exp << 3).to(torch.uint8).view(torch.float8_e4m3fn)
|
||||
gs = torch.tensor([1.5e-3], device="cuda")
|
||||
flat = swizzle_blockscale(s).contiguous().view(torch.uint8).view(-1)
|
||||
out[name] = (w, s, gs, flat)
|
||||
return out
|
||||
|
||||
|
||||
def _ll_weights(weights, flint_weights, sh):
|
||||
return moe.LLMoEWeights(
|
||||
router=weights["router"],
|
||||
experts=flint_weights,
|
||||
shared_gu=sh["gu"][0].view(torch.int32),
|
||||
shared_gu_scale=sh["gu"][3],
|
||||
shared_gu_gs=sh["gu"][2],
|
||||
shared_down=sh["down"][0].view(torch.int32),
|
||||
shared_down_scale=sh["down"][3],
|
||||
shared_down_gs=sh["down"][2],
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("num_tokens", [1, 2])
|
||||
@torch.inference_mode()
|
||||
def test_ll_moe_block(weights, flint_weights, num_tokens):
|
||||
"""The fused decode MoE block equals Flint's fused_add_rms_norm followed by
|
||||
the shared expert and the regular Flint decode MoE path."""
|
||||
from vllm.models.flint.ops import norm
|
||||
|
||||
gen = torch.Generator(device="cuda").manual_seed(5)
|
||||
bf = torch.bfloat16
|
||||
sh = _shared_nvfp4(gen)
|
||||
lw = _ll_weights(weights, flint_weights, sh)
|
||||
ln_w = (torch.rand(H, device="cuda", generator=gen) + 0.5).to(bf)
|
||||
attn = (torch.randn(num_tokens, H, device="cuda", generator=gen) * 0.5).to(bf)
|
||||
res = (torch.randn(num_tokens, H, device="cuda", generator=gen) * 2).to(bf)
|
||||
counter = moe.make_route_counter("cuda")
|
||||
out, res_out = moe.flint_moe_ll(attn, res, ln_w, 1e-6, lw, TOPK, counter)
|
||||
x, r = norm.fused_add_rms_norm(attn.clone(), res.clone(), ln_w, 1e-6)
|
||||
torch.testing.assert_close(res_out, r, rtol=0, atol=0)
|
||||
gu = (x.float() @ dequantize_nvfp4(*sh["gu"][:3]).T).to(bf).float()
|
||||
h = (torch.nn.functional.silu(gu[:, :1024]) * gu[:, 1024:]).to(bf)
|
||||
shared = (h.float() @ dequantize_nvfp4(*sh["down"][:3]).T).to(bf)
|
||||
ref = moe.flint_moe(
|
||||
x,
|
||||
weights["router"],
|
||||
flint_weights,
|
||||
TOPK,
|
||||
moe.make_route_counter("cuda"),
|
||||
shared,
|
||||
)
|
||||
match = (out == ref).float().mean().item()
|
||||
assert match >= 0.98, match
|
||||
assert _rel(out, ref) < 1e-3
|
||||
|
||||
|
||||
@pytest.mark.parametrize("preset", [-(2**31), -128, 2**31 - 128])
|
||||
@torch.inference_mode()
|
||||
def test_router_counters_wrap(weights, flint_weights, preset):
|
||||
"""The never-reset router counters elect the token's last program
|
||||
correctly across signed 32-bit wraparound (route() and the fused block)."""
|
||||
x, shared = _inputs(2, seed=3)
|
||||
counter = moe.make_route_counter("cuda")
|
||||
ref_ids, ref_tw = moe.route(x, weights["router"], TOPK, counter)
|
||||
counter.fill_(preset)
|
||||
for _ in range(3):
|
||||
ids, tw = moe.route(x, weights["router"], TOPK, counter)
|
||||
torch.testing.assert_close(ids, ref_ids, rtol=0, atol=0)
|
||||
torch.testing.assert_close(tw, ref_tw, rtol=0, atol=0)
|
||||
|
||||
gen = torch.Generator(device="cuda").manual_seed(5)
|
||||
lw = _ll_weights(weights, flint_weights, _shared_nvfp4(gen))
|
||||
ln_w = torch.ones(H, device="cuda", dtype=torch.bfloat16)
|
||||
counter = moe.make_route_counter("cuda")
|
||||
ref, _ = moe.flint_moe_ll(x, shared, ln_w, 1e-6, lw, TOPK, counter)
|
||||
counter.fill_(preset)
|
||||
for _ in range(3):
|
||||
out, _ = moe.flint_moe_ll(x, shared, ln_w, 1e-6, lw, TOPK, counter)
|
||||
torch.testing.assert_close(out, ref, rtol=0, atol=0)
|
||||
@@ -0,0 +1,204 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Flint's dense-linear and elementwise ops against high-precision references:
|
||||
the small-M NVFP4 / BF16 GEMVs (``nvfp4_gemv``, incl. the fused SiLU-and-mul
|
||||
epilogue), the large-M dequantize + cuBLAS path and global-scale check
|
||||
(``linear``), and the Triton norms and SiLU-and-mul (``norm``)."""
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from vllm.model_executor.layers.quantization.utils.nvfp4_utils import (
|
||||
swizzle_blockscale,
|
||||
)
|
||||
from vllm.models.flint.gguf_nvfp4 import dequantize_nvfp4
|
||||
from vllm.models.flint.ops import nvfp4_gemv
|
||||
from vllm.platforms import current_platform
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not current_platform.is_device_capability_family(100),
|
||||
reason="SM100 only",
|
||||
)
|
||||
|
||||
|
||||
def _cases(table):
|
||||
"""(N, K, M) for each tuned shape: M within the first token tile, and a
|
||||
partial and a full largest tile."""
|
||||
return [
|
||||
(n, k, m)
|
||||
for (n, k), cfgs in table.items()
|
||||
for m in sorted({1, 5, 8 * max(cfgs) - 3, 8 * max(cfgs)})
|
||||
]
|
||||
|
||||
|
||||
def _nvfp4(n: int, k: int, seed: int = 0):
|
||||
gen = torch.Generator(device="cuda").manual_seed(seed)
|
||||
w = torch.randint(
|
||||
0, 256, (n, k // 2), dtype=torch.uint8, device="cuda", generator=gen
|
||||
)
|
||||
# Every finite non-negative E4M3 code, subnormals and zero included.
|
||||
s = torch.randint(
|
||||
0, 127, (n, k // 16), dtype=torch.uint8, device="cuda", generator=gen
|
||||
).view(torch.float8_e4m3fn)
|
||||
gs = torch.tensor(1.7e-3, device="cuda")
|
||||
return w, s, gs
|
||||
|
||||
|
||||
def _reference(x, w, s, gs):
|
||||
return x.double() @ dequantize_nvfp4(w, s, gs).double().t()
|
||||
|
||||
|
||||
def _silu_and_mul_ref(gate_up):
|
||||
g, u = gate_up.float().chunk(2, dim=-1)
|
||||
return (g / (1 + torch.exp(-g)) * u).bfloat16()
|
||||
|
||||
|
||||
def _check(out, ref):
|
||||
# FP32 accumulation over K <= 6144 terms vs. a float64 reference: far
|
||||
# below one BF16 ulp, so the only visible error is the output rounding.
|
||||
err = (out.double() - ref).abs()
|
||||
tol = 2.0**-8 * ref.abs() + 1e-3 * ref.abs().max()
|
||||
assert (err <= tol).all(), f"max err {err.max().item()}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("n,k,m", _cases(nvfp4_gemv._TUNED))
|
||||
def test_gemv_matches_dequantized_reference(n, k, m):
|
||||
w, s, gs = _nvfp4(n, k)
|
||||
x = torch.randn(m, k, dtype=torch.bfloat16, device="cuda")
|
||||
out = nvfp4_gemv.nvfp4_gemv(x, w, swizzle_blockscale(s), gs)
|
||||
_check(out, _reference(x, w, s, gs))
|
||||
|
||||
|
||||
def test_gemv_dequantizes_every_code_exactly():
|
||||
"""A one-hot activation reads back single dequantized weights bit-exactly:
|
||||
all 16 E2M1 codes times all finite E4M3 scales, incl. subnormals."""
|
||||
n, k = 2048, 4096
|
||||
w = torch.arange(256, dtype=torch.uint8, device="cuda").repeat(n, k // 512)
|
||||
w = w[torch.randperm(n, device="cuda")]
|
||||
s = (torch.arange(n * k // 16, device="cuda") % 127).to(torch.uint8)
|
||||
s = s.reshape(n, k // 16).view(torch.float8_e4m3fn)
|
||||
gs = torch.tensor(1.7e-3, device="cuda")
|
||||
m = 16
|
||||
x = torch.zeros(m, k, dtype=torch.bfloat16, device="cuda")
|
||||
x[torch.arange(m), torch.randint(0, k, (m,))] = 1.0
|
||||
out = nvfp4_gemv.nvfp4_gemv(x, w, swizzle_blockscale(s), gs)
|
||||
ref = (x.float() @ dequantize_nvfp4(w, s, gs).t()).bfloat16()
|
||||
torch.testing.assert_close(out, ref, rtol=0, atol=0)
|
||||
|
||||
|
||||
def test_gemv_strided_input():
|
||||
n, k = 2048, 4096
|
||||
w, s, gs = _nvfp4(n, k)
|
||||
base = torch.randn(4, k + 256, dtype=torch.bfloat16, device="cuda")
|
||||
x = base[:, 128 : 128 + k]
|
||||
out = nvfp4_gemv.nvfp4_gemv(x, w, swizzle_blockscale(s), gs)
|
||||
_check(out, _reference(x, w, s, gs))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("n,k,m", _cases(nvfp4_gemv._TUNED_SILU_MUL))
|
||||
def test_gemv_silu_mul_matches_unfused(n, k, m):
|
||||
"""The fused gate_up + SiLU-and-mul epilogue equals SiLU-and-mul applied to
|
||||
the BF16-rounded GEMM output."""
|
||||
w, s, _ = _nvfp4(n, k)
|
||||
gs = torch.tensor(1e-5, device="cuda") # gate/up values of O(1-10)
|
||||
gen = torch.Generator(device="cuda").manual_seed(m)
|
||||
x = torch.randn(m, k, dtype=torch.bfloat16, device="cuda", generator=gen)
|
||||
ref = _silu_and_mul_ref(_reference(x, w, s, gs).bfloat16())
|
||||
out = nvfp4_gemv.nvfp4_gemv(x, w, swizzle_blockscale(s), gs, silu_mul=True)
|
||||
# FP32 vs float64 sums can round a gate/up value one BF16 ulp apart, which
|
||||
# SiLU amplifies for negative gates (~3% at -8).
|
||||
err = (out.float() - ref.float()).abs()
|
||||
close = err <= 2**-7 * ref.float().abs() + 1e-6
|
||||
assert close.float().mean() > 0.999
|
||||
assert (err <= 0.1 * ref.float().abs() + 1e-5).all()
|
||||
|
||||
|
||||
def test_merged_global_scale_mismatch_raises():
|
||||
from vllm.models.flint.ops.linear import check_merged_global_scales
|
||||
|
||||
p = "model.layers.0.self_attn."
|
||||
ok = [(f"{p}{n}_proj.weight_scale_2", torch.tensor(0.5)) for n in "qkv"]
|
||||
assert len(list(check_merged_global_scales(ok))) == 3
|
||||
bad = ok[:2] + [(p + "v_proj.weight_scale_2", torch.tensor(0.25))]
|
||||
with pytest.raises(ValueError):
|
||||
list(check_merged_global_scales(bad))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("n,k,m", _cases(nvfp4_gemv._TUNED_BF16))
|
||||
def test_bf16_gemv_matches_reference(n, k, m):
|
||||
w = torch.randn(n, k, dtype=torch.bfloat16, device="cuda")
|
||||
x = torch.randn(m, k, dtype=torch.bfloat16, device="cuda")
|
||||
out = nvfp4_gemv.bf16_gemv(x, w)
|
||||
_check(out, x.double() @ w.double().t())
|
||||
|
||||
|
||||
def test_bf16_gemv_silu_mul():
|
||||
n, k = 2048, 2048
|
||||
gen = torch.Generator(device="cuda").manual_seed(0)
|
||||
w = torch.randn(n, k, dtype=torch.bfloat16, device="cuda", generator=gen) * 0.05
|
||||
x = torch.randn(4, k, dtype=torch.bfloat16, device="cuda", generator=gen)
|
||||
ref = _silu_and_mul_ref((x.double() @ w.double().t()).bfloat16())
|
||||
out = nvfp4_gemv.bf16_gemv(x, w, silu_mul=True)
|
||||
err = (out.float() - ref.float()).abs()
|
||||
assert (err <= 2**-7 * ref.float().abs() + 1e-6).float().mean() > 0.999
|
||||
|
||||
|
||||
def test_large_m_dequant_gemm_matches_reference():
|
||||
from types import SimpleNamespace
|
||||
|
||||
from vllm.models.flint.ops.linear import _dequant_gemm
|
||||
|
||||
n, k = 2048, 4096
|
||||
w, s, gs = _nvfp4(n, k)
|
||||
layer = SimpleNamespace(
|
||||
weight=w,
|
||||
flint_weight_scale=swizzle_blockscale(s),
|
||||
flint_global_scale=float(gs),
|
||||
)
|
||||
x = torch.randn(256, k, dtype=torch.bfloat16, device="cuda")
|
||||
_check(_dequant_gemm(layer, x), _reference(x, w, s, gs))
|
||||
|
||||
|
||||
def _rms_norm_ref(s, w, eps):
|
||||
rs = torch.rsqrt(s.pow(2).mean(-1, keepdim=True) + eps)
|
||||
return (s * rs * w.float()).bfloat16()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("m", [1, 7, 8192])
|
||||
@pytest.mark.parametrize("has_residual", [False, True])
|
||||
@torch.inference_mode()
|
||||
def test_rms_norm_matches_reference(m, has_residual):
|
||||
"""Flint's (fused residual-add) RMSNorm: fp32 math on the unrounded
|
||||
x + residual, one bf16 rounding of each output. rsqrt.approx and the
|
||||
reduction order allow rare 1-ulp differences from the fp32 reference."""
|
||||
from vllm.models.flint.ops import norm
|
||||
|
||||
gen = torch.Generator(device="cuda").manual_seed(m)
|
||||
h = 2048
|
||||
x = (torch.randn(m, h, device="cuda", generator=gen) * 2).bfloat16()
|
||||
w = torch.empty(h, device="cuda").normal_(1, 0.2, generator=gen).bfloat16()
|
||||
if has_residual:
|
||||
r = (torch.randn(m, h, device="cuda", generator=gen) * 4).bfloat16()
|
||||
s = x.float() + r.float()
|
||||
out, res_out = norm.fused_add_rms_norm(x.clone(), r.clone(), w, 1e-6)
|
||||
torch.testing.assert_close(res_out, s.bfloat16(), rtol=0, atol=0)
|
||||
else:
|
||||
out = norm.rms_norm(x, w, 1e-6)
|
||||
s = x.float()
|
||||
ref = _rms_norm_ref(s, w, 1e-6)
|
||||
assert (out == ref).float().mean() > 0.999
|
||||
torch.testing.assert_close(out, ref, rtol=2**-7, atol=0)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("m", [1, 7, 8192])
|
||||
@torch.inference_mode()
|
||||
def test_silu_and_mul_matches_reference(m):
|
||||
"""bf16(silu(g) * u) in fp32 with one rounding."""
|
||||
from vllm.models.flint.ops import norm
|
||||
|
||||
gen = torch.Generator(device="cuda").manual_seed(m)
|
||||
x = (torch.randn(m, 2 * 6144, device="cuda", generator=gen) * 3).bfloat16()
|
||||
out = norm.silu_and_mul(x)
|
||||
ref = _silu_and_mul_ref(x)
|
||||
assert (out == ref).float().mean() > 0.999
|
||||
torch.testing.assert_close(out, ref, rtol=2**-7, atol=1e-6)
|
||||
@@ -0,0 +1,115 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Flint's NVFP4 GGUF loader must repack llama.cpp NVFP4 blocks into the
|
||||
ModelOpt layout without changing any weight value."""
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from vllm.models.flint import gguf_nvfp4
|
||||
from vllm.models.flint.gguf_nvfp4 import (
|
||||
_repack_nvfp4,
|
||||
dequantize_nvfp4,
|
||||
find_gguf_file,
|
||||
gguf_nvfp4_weights_iterator,
|
||||
)
|
||||
|
||||
gguf = pytest.importorskip("gguf", minversion="0.19.0")
|
||||
|
||||
NVFP4 = gguf.GGMLQuantizationType.NVFP4
|
||||
|
||||
|
||||
def _random_blocks(rows: int, cols: int, seed: int = 0) -> np.ndarray:
|
||||
# Random bytes cover every E2M1 code and every representable scale byte,
|
||||
# including the sign bit and the 0x7F NaN pattern that ggml decodes as 0.
|
||||
rng = np.random.default_rng(seed)
|
||||
blocks = rng.integers(0, 256, (rows, cols // 64, 36), dtype=np.uint8)
|
||||
blocks[..., :4] = np.where(blocks[..., :4] == 0xFF, 0x7F, blocks[..., :4])
|
||||
return blocks.reshape(rows, -1)
|
||||
|
||||
|
||||
def test_repack_matches_gguf_dequantize():
|
||||
rows, cols = 32, 512
|
||||
blocks = _random_blocks(rows, cols)
|
||||
weight, weight_scale = _repack_nvfp4(torch.from_numpy(blocks), cols)
|
||||
assert weight.shape == (rows, cols // 2)
|
||||
assert weight_scale.shape == (rows, cols // 16)
|
||||
ours = dequantize_nvfp4(weight, weight_scale)
|
||||
ref = gguf.quants.dequantize(blocks, NVFP4).reshape(rows, cols)
|
||||
torch.testing.assert_close(ours, torch.from_numpy(ref), atol=0, rtol=0)
|
||||
|
||||
|
||||
def test_unrepresentable_scale_raises():
|
||||
blocks = _random_blocks(1, 64)
|
||||
blocks[0, 0] = 0xFF
|
||||
with pytest.raises(ValueError):
|
||||
_repack_nvfp4(torch.from_numpy(blocks), 64)
|
||||
|
||||
|
||||
def test_iterator_maps_names_and_scales(tmp_path):
|
||||
hidden, inter, vocab, experts = 128, 64, 256, 4
|
||||
path = tmp_path / "tiny.gguf"
|
||||
writer = gguf.GGUFWriter(str(path), "flint")
|
||||
|
||||
def add_nvfp4(name, rows, cols, num, seed):
|
||||
blocks = _random_blocks(num * rows, cols, seed).reshape(num, rows, -1)
|
||||
data = blocks[0] if num == 1 else blocks
|
||||
writer.add_tensor(name + ".weight", data, raw_dtype=NVFP4)
|
||||
scale = np.full((num,), 0.5, dtype=np.float32) * np.arange(1, num + 1)
|
||||
writer.add_tensor(name + ".scale", scale)
|
||||
return data
|
||||
|
||||
q = add_nvfp4("blk.2.attn_q", hidden, hidden, 1, 1)
|
||||
add_nvfp4("blk.2.ffn_up_exps", inter, hidden, experts, 2)
|
||||
head = add_nvfp4("output", vocab, hidden, 1, 3)
|
||||
writer.add_tensor("blk.2.attn_sinks.weight", np.zeros(4, dtype=np.float32))
|
||||
writer.write_header_to_file()
|
||||
writer.write_kv_data_to_file()
|
||||
writer.write_tensors_to_file()
|
||||
writer.close()
|
||||
|
||||
tensors = dict(
|
||||
gguf_nvfp4_weights_iterator(str(path), "cpu", frozenset({"lm_head"}))
|
||||
)
|
||||
|
||||
q_w = tensors["model.layers.2.self_attn.q_proj.weight"]
|
||||
q_s = tensors["model.layers.2.self_attn.q_proj.weight_scale"]
|
||||
assert q_w.dtype == torch.uint8 and q_s.dtype == torch.float8_e4m3fn
|
||||
ref = gguf.quants.dequantize(q, NVFP4).reshape(hidden, hidden) * 0.5
|
||||
torch.testing.assert_close(
|
||||
dequantize_nvfp4(
|
||||
q_w, q_s, tensors["model.layers.2.self_attn.q_proj.weight_scale_2"]
|
||||
),
|
||||
torch.from_numpy(ref),
|
||||
atol=0,
|
||||
rtol=0,
|
||||
)
|
||||
# Stacked experts are split per expert with their own global scale.
|
||||
for e in range(experts):
|
||||
gs = tensors[f"model.layers.2.mlp.experts.{e}.up_proj.weight_scale_2"]
|
||||
assert float(gs) == 0.5 * (e + 1)
|
||||
# The LM head is dequantized exactly to bf16 (global scale kept apart).
|
||||
head_w = tensors["lm_head.weight"]
|
||||
assert head_w.dtype == torch.bfloat16
|
||||
head_ref = gguf.quants.dequantize(head, NVFP4).reshape(vocab, hidden)
|
||||
torch.testing.assert_close(
|
||||
head_w.float(), torch.from_numpy(head_ref), atol=0, rtol=0
|
||||
)
|
||||
assert float(tensors["lm_head.weight_scale_2"]) == 0.5
|
||||
assert "model.layers.2.self_attn.softmax_offset" in tensors
|
||||
|
||||
|
||||
def test_find_gguf_file_resolves_hub_repo_ids(tmp_path, monkeypatch):
|
||||
"""`vllm serve <org>/<repo>` must detect a GGUF-only repo, not just local paths."""
|
||||
(tmp_path / "flint.gguf").touch()
|
||||
monkeypatch.setattr(
|
||||
gguf_nvfp4,
|
||||
"any_pattern_in_repo_files",
|
||||
lambda repo, patterns, revision: repo == "org/gguf",
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
gguf_nvfp4, "download_weights_from_hf", lambda *args: str(tmp_path)
|
||||
)
|
||||
assert find_gguf_file("org/gguf") == str(tmp_path / "flint.gguf")
|
||||
assert find_gguf_file("org/safetensors") is None
|
||||
@@ -0,0 +1,81 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""The fused q/k norm + RoPE + cache-write kernel must reproduce the unfused
|
||||
vLLM ops (rms_norm, rotary_embedding, reshape_and_cache_flash) it replaces."""
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from vllm import _custom_ops as ops
|
||||
from vllm.models.flint.ops.qk_norm_rope import qk_norm_rope_cache
|
||||
from vllm.platforms import current_platform
|
||||
|
||||
HQ, HKV, D = 32, 4, 128
|
||||
BLOCK_SIZE, NUM_BLOCKS = 16, 64
|
||||
|
||||
|
||||
@pytest.mark.skipif(not current_platform.is_cuda(), reason="CUDA only")
|
||||
@pytest.mark.parametrize("num_tokens", [1, 7, 256, 1000])
|
||||
@pytest.mark.parametrize("apply_rope", [True, False])
|
||||
@torch.inference_mode()
|
||||
def test_matches_unfused_ops(num_tokens: int, apply_rope: bool):
|
||||
torch.manual_seed(0)
|
||||
dev = "cuda"
|
||||
qkv = torch.randn(num_tokens, (HQ + 2 * HKV) * D, device=dev).bfloat16() * 3
|
||||
positions = torch.randint(0, 100_000, (num_tokens,), device=dev)
|
||||
q_w = torch.empty(D, device=dev).normal_(1.0, 0.2).bfloat16()
|
||||
k_w = torch.empty(D, device=dev).normal_(1.0, 0.2).bfloat16()
|
||||
inv_freq = 1.0 / (10000.0 ** (torch.arange(0, D, 2, device=dev).float() / D))
|
||||
freqs = torch.outer(torch.arange(262144, device=dev).float(), inv_freq)
|
||||
cos_sin = torch.cat([freqs.cos(), freqs.sin()], dim=-1)
|
||||
# Padded tokens (beyond the slot mapping) and slot -1 must not be written.
|
||||
slots = torch.randperm(NUM_BLOCKS * BLOCK_SIZE, device=dev)[:num_tokens]
|
||||
slot_mapping = slots[: max(1, num_tokens - 1)].clone()
|
||||
slot_mapping[0] = -1
|
||||
|
||||
q, k, v = qkv.split([HQ * D, HKV * D, HKV * D], dim=-1)
|
||||
ref_q = torch.empty(num_tokens, HQ, D, device=dev, dtype=torch.bfloat16)
|
||||
ref_k = torch.empty(num_tokens, HKV, D, device=dev, dtype=torch.bfloat16)
|
||||
ops.rms_norm(ref_q, q.reshape(-1, HQ, D), q_w, 1e-6)
|
||||
ops.rms_norm(ref_k, k.reshape(-1, HKV, D), k_w, 1e-6)
|
||||
ref_q = ref_q.reshape(q.shape)
|
||||
ref_k = ref_k.reshape(k.shape)
|
||||
if apply_rope:
|
||||
# fp32 cos/sin with bf16 q/k, as in the reference implementation.
|
||||
ops.rotary_embedding(positions, ref_q, ref_k, D, cos_sin, True)
|
||||
ref_cache = torch.zeros(NUM_BLOCKS, 2, BLOCK_SIZE, HKV, D, device=dev).bfloat16()
|
||||
torch.ops._C_cache_ops.reshape_and_cache_flash(
|
||||
ref_k.reshape(-1, HKV, D),
|
||||
v.reshape(-1, HKV, D),
|
||||
ref_cache[:, 0],
|
||||
ref_cache[:, 1],
|
||||
slot_mapping,
|
||||
"auto",
|
||||
torch.ones((), device=dev),
|
||||
torch.ones((), device=dev),
|
||||
)
|
||||
|
||||
cache = torch.zeros_like(ref_cache)
|
||||
out_qkv = qkv.clone()
|
||||
out_q = qk_norm_rope_cache(
|
||||
out_qkv,
|
||||
positions,
|
||||
q_w,
|
||||
k_w,
|
||||
1e-6,
|
||||
cos_sin if apply_rope else None,
|
||||
cache[:, 0],
|
||||
cache[:, 1],
|
||||
slot_mapping,
|
||||
HQ,
|
||||
HKV,
|
||||
D,
|
||||
)
|
||||
out_k = out_qkv[:, HQ * D : (HQ + HKV) * D]
|
||||
# Same ops and roundings; only the RMS reduction order differs.
|
||||
torch.testing.assert_close(out_q, ref_q, atol=4e-3, rtol=1.6e-2)
|
||||
torch.testing.assert_close(out_k, ref_k, atol=4e-3, rtol=1.6e-2)
|
||||
assert (out_q != ref_q).float().mean() < 0.01
|
||||
assert (out_k != ref_k).float().mean() < 0.01
|
||||
torch.testing.assert_close(cache, ref_cache, atol=4e-3, rtol=1.6e-2)
|
||||
assert torch.equal(out_qkv[:, (HQ + HKV) * D :], v)
|
||||
@@ -0,0 +1,109 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""The two-kernel fused decode qkv (residual-add RMSNorm, then qkv GEMV with
|
||||
per-head q/k norm, RoPE and KV-cache write) must reproduce the kernels it
|
||||
replaces, Flint's ``fused_add_rms_norm`` / ``rms_norm`` -> Flint GEMV ->
|
||||
``qk_norm_rope_cache``, up to reduction order (rare 1-ulp differences)."""
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from vllm.platforms import current_platform
|
||||
|
||||
if not current_platform.is_cuda() or not current_platform.is_device_capability_family(
|
||||
100
|
||||
):
|
||||
pytest.skip("Flint fused qkv targets SM100", allow_module_level=True)
|
||||
|
||||
from vllm.model_executor.layers.quantization.utils.nvfp4_utils import ( # noqa: E402
|
||||
swizzle_blockscale,
|
||||
)
|
||||
from vllm.models.flint.ops import norm, nvfp4_gemv # noqa: E402
|
||||
from vllm.models.flint.ops.qk_norm_rope import qk_norm_rope_cache # noqa: E402
|
||||
from vllm.models.flint.ops.qkv_fused import flint_qkv_fused # noqa: E402
|
||||
|
||||
HQ, HKV, D, K = 32, 4, 128, 2048
|
||||
N = (HQ + 2 * HKV) * D
|
||||
NUM_BLOCKS, BLOCK_SIZE = 64, 16
|
||||
|
||||
|
||||
def _weights(bf16w: bool, g: torch.Generator):
|
||||
if bf16w:
|
||||
w = (torch.randn(N, K, device="cuda", generator=g) * 0.02).bfloat16()
|
||||
return w, None, None
|
||||
w = torch.randint(0, 256, (N, K // 2), device="cuda", generator=g)
|
||||
scales = torch.rand(N, K // 16, device="cuda", generator=g) * 2 + 0.25
|
||||
scales = swizzle_blockscale(scales.to(torch.float8_e4m3fn))
|
||||
return w.to(torch.uint8), scales, torch.tensor([0.0037], device="cuda")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("m", [1, 2, 5, 8])
|
||||
@pytest.mark.parametrize("bf16w", [False, True], ids=["nvfp4", "bf16"])
|
||||
@pytest.mark.parametrize(
|
||||
"has_residual,rope",
|
||||
[(True, True), (True, False), (False, True), (False, False)],
|
||||
ids=["res-rope", "res-norope", "first-layer", "first-norope"],
|
||||
)
|
||||
@pytest.mark.parametrize("short_slots", [False, True], ids=["pad", "short"])
|
||||
@torch.inference_mode()
|
||||
def test_matches_unfused_kernels(
|
||||
m: int, bf16w: bool, has_residual: bool, rope: bool, short_slots: bool
|
||||
):
|
||||
g = torch.Generator(device="cuda").manual_seed(m)
|
||||
w, ws, gs = _weights(bf16w, g)
|
||||
x = (torch.randn(m, K, device="cuda", generator=g) * 3).bfloat16()
|
||||
res = None
|
||||
if has_residual:
|
||||
res = (torch.randn(m, K, device="cuda", generator=g) * 3).bfloat16()
|
||||
norm_w = torch.empty(K, device="cuda").normal_(1, 0.2, generator=g).bfloat16()
|
||||
q_w = torch.empty(D, device="cuda").normal_(1, 0.2, generator=g).bfloat16()
|
||||
k_w = torch.empty(D, device="cuda").normal_(1, 0.2, generator=g).bfloat16()
|
||||
cos_sin = None
|
||||
if rope:
|
||||
inv = 1.0 / (10000.0 ** (torch.arange(0, D, 2, device="cuda").float() / D))
|
||||
freqs = torch.outer(torch.arange(262144, device="cuda").float(), inv)
|
||||
# FP32 cos | sin table, as get_rope(..., dtype=torch.float32) builds.
|
||||
cos_sin = torch.cat([freqs.cos(), freqs.sin()], dim=-1)
|
||||
# Up to the end of the RoPE table.
|
||||
positions = torch.randint(262144 - 64, 262144, (m,), device="cuda", generator=g)
|
||||
slots = torch.randperm(NUM_BLOCKS * BLOCK_SIZE, device="cuda", generator=g)[:m]
|
||||
# Padding tokens are skipped: a -1 slot, or beyond a shorter slot mapping.
|
||||
if m > 1 and short_slots:
|
||||
slots = slots[:-1]
|
||||
elif m > 1:
|
||||
slots[-1] = -1
|
||||
kv = torch.zeros(
|
||||
NUM_BLOCKS, HKV, BLOCK_SIZE, 2 * D, device="cuda", dtype=torch.bfloat16
|
||||
)
|
||||
ref_kv = kv.clone()
|
||||
|
||||
if res is not None:
|
||||
xr, ref_res = norm.fused_add_rms_norm(x.clone(), res.clone(), norm_w, 1e-6)
|
||||
else:
|
||||
xr = norm.rms_norm(x, norm_w, 1e-6)
|
||||
if bf16w:
|
||||
qkv = (xr.float() @ w.float().t()).bfloat16()
|
||||
else:
|
||||
qkv = nvfp4_gemv.nvfp4_gemv(xr, w, ws, gs)
|
||||
ref_k, ref_v = ref_kv.transpose(1, 2).split(D, dim=-1)
|
||||
ref_q = qk_norm_rope_cache(
|
||||
qkv, positions, q_w, k_w, 1e-6, cos_sin, ref_k, ref_v, slots, HQ, HKV, D
|
||||
)
|
||||
|
||||
k_cache, v_cache = kv.transpose(1, 2).split(D, dim=-1)
|
||||
q, res_out = flint_qkv_fused(
|
||||
x, res, norm_w, 1e-6, w, ws, gs, q_w, k_w, 1e-6, cos_sin, positions,
|
||||
k_cache, v_cache, slots, HQ, HKV,
|
||||
) # fmt: skip
|
||||
|
||||
if has_residual:
|
||||
torch.testing.assert_close(res_out, ref_res, atol=0, rtol=0)
|
||||
else:
|
||||
assert res_out is x
|
||||
# Same rounding points, but the norms (and the BF16 GEMV's K split) sum in
|
||||
# a different fp32 order: a 1-ulp difference in a normalized activation
|
||||
# shifts every qkv output of its token slightly.
|
||||
assert (q == ref_q).float().mean() > 0.9
|
||||
assert (kv == ref_kv).float().mean() > 0.9
|
||||
torch.testing.assert_close(q, ref_q, atol=1e-2, rtol=2e-2)
|
||||
torch.testing.assert_close(kv, ref_kv, atol=1e-2, rtol=2e-2)
|
||||
@@ -278,6 +278,7 @@ _TEXT_GENERATION_EXAMPLE_MODELS = {
|
||||
),
|
||||
"FalconForCausalLM": _HfExamplesInfo("tiiuae/falcon-7b"),
|
||||
"FalconH1ForCausalLM": _HfExamplesInfo("tiiuae/Falcon-H1-0.5B-Base"),
|
||||
"FlintForCausalLM": _HfExamplesInfo("RecursiveSI/flint", is_available_online=False),
|
||||
"FlexOlmoForCausalLM": _HfExamplesInfo("allenai/Flex-reddit-2x7B-1T"),
|
||||
"GemmaForCausalLM": _HfExamplesInfo("google/gemma-1.1-2b-it"),
|
||||
"Gemma2ForCausalLM": _HfExamplesInfo(
|
||||
|
||||
@@ -235,3 +235,12 @@ def test_speculators_rejects_local_verifier_before_model_loading(
|
||||
|
||||
with pytest.raises(ValueError, match="Speculators verifier must"):
|
||||
engine_args_cls(model=str(draft)).create_engine_config()
|
||||
|
||||
|
||||
def test_local_file_model_skips_speculators_detection(tmp_path):
|
||||
"""A single-file checkpoint (e.g. a .gguf) is not parsed as a config."""
|
||||
gguf = tmp_path / "model.gguf"
|
||||
gguf.write_bytes(b"GGUF")
|
||||
assert maybe_override_with_speculators(
|
||||
model=str(gguf), tokenizer="org/tok", trust_remote_code=False
|
||||
) == (str(gguf), "org/tok", None)
|
||||
@@ -106,6 +106,7 @@ _TEXT_GENERATION_MODELS = {
|
||||
"FalconForCausalLM": ("falcon", "FalconForCausalLM"),
|
||||
"FalconMambaForCausalLM": ("mamba", "MambaForCausalLM"),
|
||||
"FalconH1ForCausalLM": ("falcon_h1", "FalconH1ForCausalLM"),
|
||||
"FlintForCausalLM": ("vllm.models.flint", "FlintForCausalLM"),
|
||||
"GemmaForCausalLM": ("gemma", "GemmaForCausalLM"),
|
||||
"Gemma2ForCausalLM": ("gemma2", "Gemma2ForCausalLM"),
|
||||
"Gemma3ForCausalLM": ("gemma3", "Gemma3ForCausalLM"),
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Flint model entry point."""
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .model import FlintForCausalLM
|
||||
|
||||
__all__ = ["FlintForCausalLM"]
|
||||
|
||||
|
||||
def __getattr__(name: str):
|
||||
# Lazy so that resolving FlintConfig does not import CUDA kernels.
|
||||
if name in __all__:
|
||||
from . import model
|
||||
|
||||
return getattr(model, name)
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
@@ -0,0 +1,92 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Flint model config."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from transformers.configuration_utils import PreTrainedConfig
|
||||
|
||||
|
||||
class FlintConfig(PreTrainedConfig):
|
||||
"""Decoder-only MoE transformer with hybrid sliding/full attention.
|
||||
|
||||
Sliding layers use RoPE; full-attention layers listed in
|
||||
``no_rope_layer_indices`` use no positional encoding. Every attention head
|
||||
has a learned sink logit. ``sliding_window`` counts the current token.
|
||||
"""
|
||||
|
||||
model_type = "flint"
|
||||
keys_to_ignore_at_inference = ["past_key_values"]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
vocab_size: int = 150144,
|
||||
hidden_size: int = 2048,
|
||||
intermediate_size: int = 6144,
|
||||
num_hidden_layers: int = 49,
|
||||
num_attention_heads: int = 32,
|
||||
num_key_value_heads: int = 4,
|
||||
head_dim: int = 128,
|
||||
hidden_act: str = "silu",
|
||||
max_position_embeddings: int = 262144,
|
||||
initializer_range: float = 0.02,
|
||||
rms_norm_eps: float = 1e-6,
|
||||
use_cache: bool = True,
|
||||
tie_word_embeddings: bool = False,
|
||||
rope_theta: float = 10000.0,
|
||||
rope_parameters: dict[str, Any] | None = None,
|
||||
attention_bias: bool = False,
|
||||
attention_dropout: float = 0.0,
|
||||
use_sliding_window: bool = True,
|
||||
sliding_window: int | None = 513,
|
||||
layer_types: list[str] | None = None,
|
||||
no_rope_layer_indices: list[int] | None = None,
|
||||
moe_intermediate_size: int = 512,
|
||||
shared_expert_intermediate_size: int = 1024,
|
||||
num_experts_per_tok: int = 6,
|
||||
num_experts: int = 256,
|
||||
norm_topk_prob: bool = True,
|
||||
router_score_function: str = "sigmoid",
|
||||
mlp_only_layers: list[int] | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
self.vocab_size = vocab_size
|
||||
self.hidden_size = hidden_size
|
||||
self.intermediate_size = intermediate_size
|
||||
self.num_hidden_layers = num_hidden_layers
|
||||
self.num_attention_heads = num_attention_heads
|
||||
self.num_key_value_heads = num_key_value_heads
|
||||
self.head_dim = head_dim
|
||||
self.hidden_act = hidden_act
|
||||
self.max_position_embeddings = max_position_embeddings
|
||||
self.initializer_range = initializer_range
|
||||
self.rms_norm_eps = rms_norm_eps
|
||||
self.use_cache = use_cache
|
||||
self.rope_theta = rope_theta
|
||||
self.rope_parameters = rope_parameters or {
|
||||
"rope_type": "default",
|
||||
"rope_theta": rope_theta,
|
||||
}
|
||||
self.attention_bias = attention_bias
|
||||
self.attention_dropout = attention_dropout
|
||||
self.use_sliding_window = use_sliding_window
|
||||
self.sliding_window = sliding_window if use_sliding_window else None
|
||||
self.layer_types = layer_types or ["full_attention"] * num_hidden_layers
|
||||
if len(self.layer_types) != num_hidden_layers:
|
||||
raise ValueError(
|
||||
f"layer_types has {len(self.layer_types)} entries but "
|
||||
f"num_hidden_layers is {num_hidden_layers}"
|
||||
)
|
||||
self.no_rope_layer_indices = no_rope_layer_indices or []
|
||||
self.moe_intermediate_size = moe_intermediate_size
|
||||
self.shared_expert_intermediate_size = shared_expert_intermediate_size
|
||||
self.num_experts_per_tok = num_experts_per_tok
|
||||
self.num_experts = num_experts
|
||||
self.norm_topk_prob = norm_topk_prob
|
||||
self.router_score_function = router_score_function
|
||||
self.mlp_only_layers = mlp_only_layers or []
|
||||
# The reference implementation computes the LM head in fp32.
|
||||
super().__init__(tie_word_embeddings=tie_word_embeddings, **kwargs)
|
||||
|
||||
|
||||
__all__ = ["FlintConfig"]
|
||||
@@ -0,0 +1,223 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Load Flint's NVFP4 GGUF checkpoint as ModelOpt-layout NVFP4 tensors.
|
||||
|
||||
A GGUF NVFP4 super-block holds 64 values as four 16-value sub-blocks, each with
|
||||
a UE4M3 scale byte and 8 bytes of E2M1 codes (low nibbles = values 0..7, high
|
||||
nibbles = values 8..15). The value is ``e4m3(scale) * e2m1(code) * tensor_scale``
|
||||
where ``tensor_scale`` is the companion ``<name>.scale`` F32 tensor (one per
|
||||
expert for stacked expert tensors). ModelOpt packs consecutive values into one
|
||||
byte (low nibble first) and stores the same three factors as ``weight``,
|
||||
``weight_scale`` and ``weight_scale_2``, so conversion is a lossless repack.
|
||||
"""
|
||||
|
||||
import glob
|
||||
import os
|
||||
import warnings
|
||||
from collections.abc import Iterator
|
||||
|
||||
import huggingface_hub
|
||||
import numpy as np
|
||||
import regex as re
|
||||
import torch
|
||||
|
||||
from vllm.model_executor.model_loader.weight_utils import download_weights_from_hf
|
||||
from vllm.transformers_utils.repo_utils import any_pattern_in_repo_files
|
||||
|
||||
_GGUF_NVFP4 = 40
|
||||
_QK = 64
|
||||
_BLOCK_BYTES = 36
|
||||
|
||||
_LAYER_NAMES = {
|
||||
"attn_norm.weight": "input_layernorm.weight",
|
||||
"ffn_norm.weight": "post_attention_layernorm.weight",
|
||||
"attn_q_norm.weight": "self_attn.q_norm.weight",
|
||||
"attn_k_norm.weight": "self_attn.k_norm.weight",
|
||||
"attn_sinks.weight": "self_attn.softmax_offset",
|
||||
"attn_q": "self_attn.q_proj",
|
||||
"attn_k": "self_attn.k_proj",
|
||||
"attn_v": "self_attn.v_proj",
|
||||
"attn_output": "self_attn.o_proj",
|
||||
"ffn_gate": "mlp.gate_proj",
|
||||
"ffn_up": "mlp.up_proj",
|
||||
"ffn_down": "mlp.down_proj",
|
||||
"ffn_gate_shexp": "mlp.shared_expert.gate_proj",
|
||||
"ffn_up_shexp": "mlp.shared_expert.up_proj",
|
||||
"ffn_down_shexp": "mlp.shared_expert.down_proj",
|
||||
"ffn_gate_inp.weight": "mlp.gate.weight",
|
||||
}
|
||||
_EXPERT_NAMES = {
|
||||
"ffn_gate_exps": "gate_proj",
|
||||
"ffn_up_exps": "up_proj",
|
||||
"ffn_down_exps": "down_proj",
|
||||
}
|
||||
_GLOBAL_NAMES = {
|
||||
"token_embd.weight": "model.embed_tokens.weight",
|
||||
"output_norm.weight": "model.norm.weight",
|
||||
"output": "lm_head",
|
||||
}
|
||||
|
||||
|
||||
def find_gguf_file(
|
||||
model_path: str, revision: str | None = None, download_dir: str | None = None
|
||||
) -> str | None:
|
||||
if os.path.isfile(model_path) and model_path.endswith(".gguf"):
|
||||
return model_path
|
||||
if not os.path.isdir(model_path):
|
||||
# Offline, the repo cannot be listed; look in the cached snapshot instead.
|
||||
offline = huggingface_hub.constants.HF_HUB_OFFLINE
|
||||
if not offline and not any_pattern_in_repo_files(
|
||||
model_path, ["*.gguf"], revision=revision
|
||||
):
|
||||
return None
|
||||
try:
|
||||
model_path = download_weights_from_hf(
|
||||
model_path, download_dir, ["*.gguf"], revision
|
||||
)
|
||||
except huggingface_hub.errors.LocalEntryNotFoundError:
|
||||
return None
|
||||
files = sorted(glob.glob(os.path.join(model_path, "*.gguf")))
|
||||
if len(files) > 1:
|
||||
raise ValueError(f"Expected one GGUF file in {model_path}, found {files}")
|
||||
return files[0] if files else None
|
||||
|
||||
|
||||
def _repack_nvfp4(
|
||||
blocks: torch.Tensor, n_cols: int
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
"""[..., n_cols / 64 * 36] GGUF bytes -> (packed [..., n_cols / 2] uint8,
|
||||
block scales [..., n_cols / 16] float8_e4m3fn)."""
|
||||
lead = blocks.shape[:-1]
|
||||
blocks = blocks.reshape(*lead, n_cols // _QK, _BLOCK_BYTES)
|
||||
# ggml decodes the scales as unsigned E4M3 and the NaN pattern as zero;
|
||||
# 0xFF would decode to 480, which E4M3 cannot represent.
|
||||
scales = blocks[..., :4]
|
||||
if bool((scales == 0xFF).any()):
|
||||
raise ValueError("NVFP4 GGUF block scale 0xFF is not supported")
|
||||
scales = torch.where(scales == 0x7F, torch.zeros_like(scales), scales & 0x7F)
|
||||
qs = blocks[..., 4:].reshape(*lead, n_cols // _QK, 4, 8)
|
||||
codes = torch.cat([qs & 0x0F, qs >> 4], dim=-1)
|
||||
packed = codes[..., 0::2] | (codes[..., 1::2] << 4)
|
||||
return (
|
||||
packed.reshape(*lead, n_cols // 2).contiguous(),
|
||||
scales.reshape(*lead, n_cols // 16).contiguous().view(torch.float8_e4m3fn),
|
||||
)
|
||||
|
||||
|
||||
_E2M1_POS = (0.0, 0.5, 1.0, 1.5, 2.0, 3.0, 4.0, 6.0)
|
||||
_E2M1 = _E2M1_POS + tuple(-v for v in _E2M1_POS)
|
||||
|
||||
|
||||
def dequantize_nvfp4(
|
||||
weight: torch.Tensor,
|
||||
weight_scale: torch.Tensor,
|
||||
global_scale: torch.Tensor | None = None,
|
||||
out_dtype: torch.dtype = torch.float32,
|
||||
) -> torch.Tensor:
|
||||
"""ModelOpt-layout NVFP4 [N, K / 2] -> [N, K]. Without ``global_scale`` the
|
||||
result is ``e2m1 * e4m3``, which is exact in bf16."""
|
||||
chunk_rows = 8192
|
||||
lut = torch.tensor(_E2M1, dtype=torch.float32, device=weight.device)
|
||||
out = torch.empty(
|
||||
(*weight.shape[:-1], weight.shape[-1] * 2),
|
||||
dtype=out_dtype,
|
||||
device=weight.device,
|
||||
)
|
||||
for start in range(0, weight.shape[0], chunk_rows):
|
||||
w = weight[start : start + chunk_rows]
|
||||
codes = torch.stack([w & 0x0F, w >> 4], dim=-1).int()
|
||||
values = lut[codes].reshape(*w.shape[:-1], -1, 16)
|
||||
values = values * weight_scale[start : start + chunk_rows].float().unsqueeze(-1)
|
||||
values = values.flatten(-2)
|
||||
if global_scale is not None:
|
||||
values = values * global_scale.float().to(weight.device)
|
||||
out[start : start + chunk_rows] = values
|
||||
return out
|
||||
|
||||
|
||||
def _as_tensor(data: np.ndarray) -> torch.Tensor:
|
||||
# The reader memory-maps the file read-only; tensors are only read.
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore", UserWarning)
|
||||
return torch.from_numpy(np.asarray(data))
|
||||
|
||||
|
||||
def _map_name(name: str) -> tuple[str, bool]:
|
||||
"""GGUF tensor base name -> (HF base name, is_stacked_experts)."""
|
||||
if name in _GLOBAL_NAMES:
|
||||
return _GLOBAL_NAMES[name], False
|
||||
m = re.fullmatch(r"blk\.(\d+)\.(.+)", name)
|
||||
if m is None:
|
||||
raise ValueError(f"Unexpected tensor in Flint GGUF: {name}")
|
||||
layer, rest = m.groups()
|
||||
prefix = f"model.layers.{layer}."
|
||||
if rest in _EXPERT_NAMES:
|
||||
return prefix + "mlp.experts.{}." + _EXPERT_NAMES[rest], True
|
||||
if rest not in _LAYER_NAMES:
|
||||
raise ValueError(f"Unexpected tensor in Flint GGUF: {name}")
|
||||
return prefix + _LAYER_NAMES[rest], False
|
||||
|
||||
|
||||
def gguf_nvfp4_weights_iterator(
|
||||
path: str,
|
||||
device: torch.device | str = "cuda",
|
||||
dequantize: frozenset[str] = frozenset(),
|
||||
) -> Iterator[tuple[str, torch.Tensor]]:
|
||||
"""Yield HF-style (name, tensor) pairs; NVFP4 tensors become ``.weight``
|
||||
(packed uint8), ``.weight_scale`` (E4M3) and ``.weight_scale_2`` (F32),
|
||||
except HF base names in ``dequantize``, which become a BF16 ``.weight``
|
||||
holding ``e2m1 * e4m3`` (exact) plus the F32 ``.weight_scale_2`` that the
|
||||
caller applies to the fp32 output."""
|
||||
try:
|
||||
import gguf
|
||||
except ImportError as err:
|
||||
raise ImportError(
|
||||
"Loading Flint NVFP4 GGUF checkpoints requires the `gguf` package "
|
||||
"(pip install 'gguf>=0.19.0')."
|
||||
) from err
|
||||
|
||||
reader = gguf.GGUFReader(path)
|
||||
prec_a4 = reader.fields.get("general.tensor_extra.prec_a4")
|
||||
if prec_a4 is not None and any(prec_a4.contents()):
|
||||
raise ValueError(
|
||||
"GGUF marks tensors with FP4 activations (prec_a4); only weight-only "
|
||||
"NVFP4 (W4A16) is supported."
|
||||
)
|
||||
tensors = {t.name: t for t in reader.tensors}
|
||||
for name, t in tensors.items():
|
||||
if name.endswith(".scale"):
|
||||
continue
|
||||
data = _as_tensor(t.data)
|
||||
if t.tensor_type != _GGUF_NVFP4:
|
||||
hf_name, _ = _map_name(name)
|
||||
if t.tensor_type == gguf.GGMLQuantizationType.BF16:
|
||||
data = data.view(torch.bfloat16)
|
||||
elif t.tensor_type != gguf.GGMLQuantizationType.F32:
|
||||
raise ValueError(
|
||||
f"Unsupported GGUF tensor type for {name}: {t.tensor_type}"
|
||||
)
|
||||
yield hf_name, data
|
||||
continue
|
||||
|
||||
base = name.removesuffix(".weight")
|
||||
hf_base, stacked = _map_name(base)
|
||||
n_cols = int(t.shape[0])
|
||||
weight, weight_scale = _repack_nvfp4(data.to(device), n_cols)
|
||||
global_scale = _as_tensor(tensors[base + ".scale"].data)
|
||||
if hf_base in dequantize:
|
||||
yield (
|
||||
hf_base + ".weight",
|
||||
dequantize_nvfp4(weight, weight_scale, out_dtype=torch.bfloat16),
|
||||
)
|
||||
yield hf_base + ".weight_scale_2", global_scale.reshape(())
|
||||
continue
|
||||
if not stacked:
|
||||
yield hf_base + ".weight", weight
|
||||
yield hf_base + ".weight_scale", weight_scale
|
||||
yield hf_base + ".weight_scale_2", global_scale.reshape(())
|
||||
continue
|
||||
for e in range(weight.shape[0]):
|
||||
expert = hf_base.format(e)
|
||||
yield expert + ".weight", weight[e]
|
||||
yield expert + ".weight_scale", weight_scale[e]
|
||||
yield expert + ".weight_scale_2", global_scale[e]
|
||||
@@ -0,0 +1,693 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Inference-only Flint model (BF16 safetensors or NVFP4 GGUF checkpoints).
|
||||
|
||||
Flint is a Qwen3-MoE-style decoder with learned attention sinks, hybrid
|
||||
sliding-window/full attention (full layers use no RoPE), dense SwiGLU MLPs on
|
||||
``mlp_only_layers``, sigmoid top-k routing with renormalized weights, and an
|
||||
ungated shared expert.
|
||||
"""
|
||||
|
||||
from collections.abc import Iterable
|
||||
|
||||
import torch
|
||||
from torch import nn
|
||||
|
||||
from vllm.config import (
|
||||
CacheConfig,
|
||||
ParallelConfig,
|
||||
VllmConfig,
|
||||
get_current_vllm_config,
|
||||
)
|
||||
from vllm.distributed import get_tensor_model_parallel_world_size
|
||||
from vllm.forward_context import get_forward_context
|
||||
from vllm.model_executor.layers.attention import Attention
|
||||
from vllm.model_executor.layers.attention.attention import (
|
||||
unified_attention_with_output,
|
||||
)
|
||||
from vllm.model_executor.layers.fused_moe import FusedMoEFactory, GateLinear
|
||||
from vllm.model_executor.layers.linear import (
|
||||
MergedColumnParallelLinear,
|
||||
QKVParallelLinear,
|
||||
RowParallelLinear,
|
||||
)
|
||||
from vllm.model_executor.layers.logits_processor import LogitsProcessor
|
||||
from vllm.model_executor.layers.quantization import QuantizationConfig
|
||||
from vllm.model_executor.layers.quantization.modelopt import ModelOptNvFp4Config
|
||||
from vllm.model_executor.layers.rotary_embedding import get_rope
|
||||
from vllm.model_executor.layers.vocab_parallel_embedding import (
|
||||
ParallelLMHead,
|
||||
VocabParallelEmbedding,
|
||||
)
|
||||
from vllm.model_executor.model_loader.weight_utils import make_attention_sink
|
||||
from vllm.model_executor.models.interfaces import SupportsPP
|
||||
from vllm.model_executor.models.utils import (
|
||||
AutoWeightsLoader,
|
||||
WeightsMapper,
|
||||
extract_layer_index,
|
||||
make_empty_intermediate_tensors_factory,
|
||||
make_layers,
|
||||
maybe_prefix,
|
||||
)
|
||||
from vllm.platforms import current_platform
|
||||
from vllm.sequence import IntermediateTensors
|
||||
|
||||
from .configs import FlintConfig
|
||||
from .gguf_nvfp4 import find_gguf_file, gguf_nvfp4_weights_iterator
|
||||
from .ops import linear as flint_linear
|
||||
from .ops.moe import (
|
||||
BF16_DECODE_MAX_TOKENS,
|
||||
LL_MOE_MAX_TOKENS,
|
||||
FlintBf16DecodeMoE,
|
||||
FlintNvfp4Experts,
|
||||
LLMoEWeights,
|
||||
flint_moe_ll,
|
||||
ll_weights_from_layers,
|
||||
)
|
||||
from .ops.norm import RMSNorm, silu_and_mul
|
||||
from .ops.qk_norm_rope import qk_norm_rope_cache
|
||||
from .ops.qkv_fused import MAX_M as LL_MAX_TOKENS
|
||||
from .ops.qkv_fused import flint_qkv_fused
|
||||
|
||||
|
||||
def _flint_kernels(
|
||||
quant_config: QuantizationConfig | None, parallel: ParallelConfig | None = None
|
||||
) -> bool:
|
||||
"""Flint's kernels serve the BF16 and NVFP4 checkpoints on a single SM100
|
||||
GPU (TP=1, and for the MoE no EP/EPLB); other configurations use vLLM's
|
||||
stock layers."""
|
||||
if not current_platform.is_device_capability_family(100):
|
||||
return False
|
||||
if get_tensor_model_parallel_world_size() != 1 or not (
|
||||
quant_config is None or isinstance(quant_config, ModelOptNvFp4Config)
|
||||
):
|
||||
return False
|
||||
return parallel is None or not (
|
||||
parallel.enable_expert_parallel or parallel.enable_eplb
|
||||
)
|
||||
|
||||
|
||||
# Dequantized to BF16 (without the global scale, so exactly) at load so logits
|
||||
# can be accumulated in fp32; the global scale is applied to the fp32 logits.
|
||||
_GGUF_DEQUANTIZED = frozenset({"lm_head"})
|
||||
|
||||
|
||||
def _nvfp4_gguf_quant_config() -> QuantizationConfig:
|
||||
# The GGUF reference runtime keeps activations in 16 bits (W4A16).
|
||||
return ModelOptNvFp4Config(
|
||||
quant_method="W4A16_NVFP4",
|
||||
is_checkpoint_nvfp4_serialized=True,
|
||||
exclude_modules=["model.embed_tokens", "*.mlp.gate", *_GGUF_DEQUANTIZED],
|
||||
group_size=16,
|
||||
)
|
||||
|
||||
|
||||
class FlintMLP(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
hidden_size: int,
|
||||
intermediate_size: int,
|
||||
quant_config: QuantizationConfig | None,
|
||||
reduce_results: bool = True,
|
||||
prefix: str = "",
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.gate_up_proj = MergedColumnParallelLinear(
|
||||
hidden_size,
|
||||
[intermediate_size] * 2,
|
||||
bias=False,
|
||||
quant_config=quant_config,
|
||||
prefix=f"{prefix}.gate_up_proj",
|
||||
)
|
||||
self.down_proj = RowParallelLinear(
|
||||
intermediate_size,
|
||||
hidden_size,
|
||||
bias=False,
|
||||
quant_config=quant_config,
|
||||
reduce_results=reduce_results,
|
||||
prefix=f"{prefix}.down_proj",
|
||||
)
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
act = flint_linear.gate_up_silu_mul(self.gate_up_proj, x)
|
||||
if act is None:
|
||||
gate_up, _ = self.gate_up_proj(x)
|
||||
act = silu_and_mul(gate_up)
|
||||
x, _ = self.down_proj(act)
|
||||
return x
|
||||
|
||||
|
||||
class FlintSparseMoeBlock(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
config: FlintConfig,
|
||||
quant_config: QuantizationConfig | None,
|
||||
prefix: str = "",
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.top_k = config.num_experts_per_tok
|
||||
self.gate = GateLinear(
|
||||
config.hidden_size,
|
||||
config.num_experts,
|
||||
quant_config=None,
|
||||
prefix=f"{prefix}.gate",
|
||||
)
|
||||
self.shared_expert = FlintMLP(
|
||||
hidden_size=config.hidden_size,
|
||||
intermediate_size=config.shared_expert_intermediate_size,
|
||||
quant_config=quant_config,
|
||||
reduce_results=False,
|
||||
prefix=f"{prefix}.shared_expert",
|
||||
)
|
||||
vllm_config = get_current_vllm_config()
|
||||
# NVFP4: Flint's MoE for all batch sizes. BF16: Flint's decode kernels
|
||||
# for small batches on the TRTLLM-layout experts, TRTLLM MoE otherwise.
|
||||
is_nvfp4 = isinstance(quant_config, ModelOptNvFp4Config)
|
||||
use_flint = _flint_kernels(quant_config, vllm_config.parallel_config)
|
||||
if use_flint:
|
||||
assert config.router_score_function == "sigmoid"
|
||||
assert config.norm_topk_prob
|
||||
self.use_flint_kernel = use_flint and is_nvfp4
|
||||
self.bf16_decode: FlintBf16DecodeMoE | None = None
|
||||
self._ll_weights: LLMoEWeights | None = None
|
||||
if self.use_flint_kernel:
|
||||
self.experts = FlintNvfp4Experts(
|
||||
config.num_experts,
|
||||
config.hidden_size,
|
||||
config.moe_intermediate_size,
|
||||
config.num_experts_per_tok,
|
||||
vllm_config.scheduler_config.max_num_batched_tokens,
|
||||
)
|
||||
return
|
||||
self.experts = FusedMoEFactory(
|
||||
shared_experts=self.shared_expert,
|
||||
gate=self.gate,
|
||||
num_experts=config.num_experts,
|
||||
top_k=config.num_experts_per_tok,
|
||||
hidden_size=config.hidden_size,
|
||||
intermediate_size=config.moe_intermediate_size,
|
||||
renormalize=config.norm_topk_prob,
|
||||
scoring_func=config.router_score_function,
|
||||
quant_config=quant_config,
|
||||
prefix=f"{prefix}.experts",
|
||||
)
|
||||
if use_flint:
|
||||
self.bf16_decode = FlintBf16DecodeMoE(
|
||||
config.num_experts,
|
||||
config.hidden_size,
|
||||
config.moe_intermediate_size,
|
||||
config.num_experts_per_tok,
|
||||
)
|
||||
|
||||
def forward_ll(
|
||||
self, attn_out: torch.Tensor, residual: torch.Tensor, norm: RMSNorm
|
||||
) -> tuple[torch.Tensor, torch.Tensor] | None:
|
||||
"""``(mlp(norm(attn_out + residual)), attn_out + residual)`` via the
|
||||
fused decode MoE block (NVFP4, M <= LL_MOE_MAX_TOKENS), else None."""
|
||||
if not self.use_flint_kernel or attn_out.shape[0] > LL_MOE_MAX_TOKENS:
|
||||
return None
|
||||
if self._ll_weights is None:
|
||||
assert self.experts.weights is not None
|
||||
self._ll_weights = ll_weights_from_layers(
|
||||
self.gate.weight,
|
||||
self.shared_expert.gate_up_proj,
|
||||
self.shared_expert.down_proj,
|
||||
self.experts.weights,
|
||||
)
|
||||
return flint_moe_ll(
|
||||
attn_out,
|
||||
residual,
|
||||
norm.weight,
|
||||
norm.variance_epsilon,
|
||||
self._ll_weights,
|
||||
self.top_k,
|
||||
self.experts.route_counter,
|
||||
)
|
||||
|
||||
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
|
||||
if self.use_flint_kernel:
|
||||
return self.experts(hidden_states, self.gate.weight, self.shared_expert)
|
||||
if (
|
||||
self.bf16_decode is not None
|
||||
and hidden_states.shape[0] <= BF16_DECODE_MAX_TOKENS
|
||||
):
|
||||
return self.bf16_decode(
|
||||
hidden_states,
|
||||
self.gate.weight,
|
||||
self.experts.routed_experts,
|
||||
self.shared_expert,
|
||||
)
|
||||
return self.experts(hidden_states=hidden_states, router_logits=hidden_states)
|
||||
|
||||
|
||||
class FlintAttention(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
config: FlintConfig,
|
||||
layer_idx: int,
|
||||
cache_config: CacheConfig | None,
|
||||
quant_config: QuantizationConfig | None,
|
||||
prefix: str = "",
|
||||
) -> None:
|
||||
super().__init__()
|
||||
tp_size = get_tensor_model_parallel_world_size()
|
||||
self.head_dim = config.head_dim
|
||||
self.total_num_heads = config.num_attention_heads
|
||||
self.total_num_kv_heads = config.num_key_value_heads
|
||||
assert self.total_num_heads % tp_size == 0
|
||||
self.num_heads = self.total_num_heads // tp_size
|
||||
self.num_kv_heads = max(1, self.total_num_kv_heads // tp_size)
|
||||
self.q_size = self.num_heads * self.head_dim
|
||||
self.kv_size = self.num_kv_heads * self.head_dim
|
||||
|
||||
self.qkv_proj = QKVParallelLinear(
|
||||
config.hidden_size,
|
||||
self.head_dim,
|
||||
self.total_num_heads,
|
||||
self.total_num_kv_heads,
|
||||
bias=config.attention_bias,
|
||||
quant_config=quant_config,
|
||||
prefix=f"{prefix}.qkv_proj",
|
||||
)
|
||||
self.o_proj = RowParallelLinear(
|
||||
self.total_num_heads * self.head_dim,
|
||||
config.hidden_size,
|
||||
bias=False,
|
||||
quant_config=quant_config,
|
||||
prefix=f"{prefix}.o_proj",
|
||||
)
|
||||
self.q_norm = RMSNorm(self.head_dim, eps=config.rms_norm_eps)
|
||||
self.k_norm = RMSNorm(self.head_dim, eps=config.rms_norm_eps)
|
||||
|
||||
self.rotary_emb = None
|
||||
if layer_idx not in config.no_rope_layer_indices:
|
||||
# Only its cos|sin table is used, by the fused kernels. Kept in
|
||||
# fp32 like the reference implementation; get_rope memoizes, so
|
||||
# all layers share one table.
|
||||
self.rotary_emb = get_rope(
|
||||
self.head_dim,
|
||||
max_position=config.max_position_embeddings,
|
||||
rope_parameters=config.rope_parameters,
|
||||
dtype=torch.float32,
|
||||
)
|
||||
assert self.rotary_emb.is_neox_style
|
||||
assert self.rotary_emb.rotary_dim == self.head_dim
|
||||
is_sliding = config.layer_types[layer_idx] == "sliding_attention"
|
||||
self.softmax_offset = make_attention_sink(self.num_heads, torch.float32)
|
||||
self.attn = Attention(
|
||||
self.num_heads,
|
||||
self.head_dim,
|
||||
self.head_dim**-0.5,
|
||||
num_kv_heads=self.num_kv_heads,
|
||||
cache_config=cache_config,
|
||||
quant_config=quant_config,
|
||||
per_layer_sliding_window=config.sliding_window if is_sliding else None,
|
||||
prefix=f"{prefix}.attn",
|
||||
sinks=self.softmax_offset,
|
||||
)
|
||||
# The fused kernel writes the KV cache itself (in the FlashInfer layout)
|
||||
# and then calls the attention backend directly. Imported here so the
|
||||
# module imports on platforms without FlashInfer.
|
||||
from vllm.v1.attention.backends.flashinfer import (
|
||||
FlashInferImpl,
|
||||
FlashInferMetadata,
|
||||
FlashInferTrtllmAPIDecode,
|
||||
)
|
||||
|
||||
impl = self.attn.impl
|
||||
assert isinstance(impl, FlashInferImpl), "Flint requires FlashInfer attention"
|
||||
self._metadata_cls = FlashInferMetadata
|
||||
self._decode_cls = FlashInferTrtllmAPIDecode
|
||||
assert get_current_vllm_config().model_config.dtype == torch.bfloat16
|
||||
assert self.attn.kv_cache_dtype in ("auto", "bfloat16")
|
||||
assert impl.kv_sharing_target_layer_name is None
|
||||
self.impl = impl
|
||||
# Flint decode attention and the fused low-latency decode path.
|
||||
self.flint_decode = _flint_kernels(quant_config)
|
||||
# Largest pure-decode batch served by the Flint kernel; beyond it
|
||||
# FlashInfer is faster on the full-attention layers at long context.
|
||||
self.max_decode_batch = 64 if is_sliding else 8
|
||||
if self.flint_decode:
|
||||
assert not config.attention_bias
|
||||
from .ops.decode_attention import flint_decode_attention
|
||||
|
||||
self._decode_attention = flint_decode_attention
|
||||
|
||||
def forward(
|
||||
self, positions: torch.Tensor, hidden_states: torch.Tensor
|
||||
) -> torch.Tensor:
|
||||
qkv, _ = self.qkv_proj(hidden_states)
|
||||
k_cache, v_cache, layer_slots = self._cache_views()
|
||||
plan = self._decode_plan() if self.flint_decode else None
|
||||
q = qk_norm_rope_cache(
|
||||
qkv,
|
||||
positions,
|
||||
self.q_norm.weight,
|
||||
self.k_norm.weight,
|
||||
self.q_norm.variance_epsilon,
|
||||
self._cos_sin(),
|
||||
k_cache,
|
||||
v_cache,
|
||||
layer_slots,
|
||||
self.num_heads,
|
||||
self.num_kv_heads,
|
||||
self.head_dim,
|
||||
# Flint decode attention waits on the grid dependency before
|
||||
# reading q / K / V, so it may start its prologue early.
|
||||
launch_dependents=plan is not None,
|
||||
)
|
||||
num_tokens = qkv.shape[0]
|
||||
attn_output = torch.empty_like(q)
|
||||
if plan is not None:
|
||||
self._run_decode_attention(q, attn_output, *plan)
|
||||
else:
|
||||
_, k, v = qkv.split([self.q_size, self.kv_size, self.kv_size], dim=-1)
|
||||
unified_attention_with_output(
|
||||
q.view(num_tokens, self.num_heads, self.head_dim),
|
||||
k.view(num_tokens, self.num_kv_heads, self.head_dim),
|
||||
v.view(num_tokens, self.num_kv_heads, self.head_dim),
|
||||
attn_output.view(num_tokens, self.num_heads, self.head_dim),
|
||||
self.attn.layer_name,
|
||||
)
|
||||
output, _ = self.o_proj(attn_output)
|
||||
return output
|
||||
|
||||
def _cos_sin(self) -> torch.Tensor | None:
|
||||
return None if self.rotary_emb is None else self.rotary_emb.cos_sin_cache
|
||||
|
||||
def _cache_views(
|
||||
self,
|
||||
) -> tuple[torch.Tensor | None, torch.Tensor | None, torch.Tensor | None]:
|
||||
"""(k_cache, v_cache, slot_mapping) for the fused cache write, with the
|
||||
caches as [blocks, block_size, kv_heads, head_dim] views; all None while
|
||||
the KV cache is unallocated (memory profiling)."""
|
||||
kv_cache = self.attn.kv_cache
|
||||
if kv_cache.numel() == 0:
|
||||
return None, None, None
|
||||
# As in FlashInferImpl.do_kv_cache_update: (B, H, N, 2D) -> (B, N, H, D)
|
||||
k_cache, v_cache = kv_cache.transpose(1, 2).split(self.head_dim, dim=-1)
|
||||
slot_mapping = get_forward_context().slot_mapping
|
||||
assert isinstance(slot_mapping, dict)
|
||||
return k_cache, v_cache, slot_mapping.get(self.attn.layer_name)
|
||||
|
||||
def _decode_plan(self) -> tuple | None:
|
||||
"""(decode metadata, num_decodes, window) when the batch is pure
|
||||
single-token decode of at most ``max_decode_batch`` requests (the Flint
|
||||
decode attention kernel), else None. K/V are already in the cache."""
|
||||
forward_metadata = get_forward_context().attn_metadata
|
||||
attn_metadata = (
|
||||
forward_metadata.get(self.attn.layer_name)
|
||||
if isinstance(forward_metadata, dict)
|
||||
else forward_metadata
|
||||
)
|
||||
if (
|
||||
not isinstance(attn_metadata, self._metadata_cls) # profiling run
|
||||
or attn_metadata.num_prefills != 0
|
||||
or not 0 < attn_metadata.num_decodes <= self.max_decode_batch
|
||||
or attn_metadata.num_decode_tokens != attn_metadata.num_decodes
|
||||
):
|
||||
return None
|
||||
decode = attn_metadata.decode
|
||||
assert isinstance(decode, self._decode_cls)
|
||||
assert decode.q_len_per_req == 1 and decode.dcp_query_start_loc is None
|
||||
left = self.impl.window_left
|
||||
window = left + 1 if left >= 0 else None
|
||||
return decode, attn_metadata.num_decodes, window
|
||||
|
||||
def _run_decode_attention(
|
||||
self, q: torch.Tensor, out: torch.Tensor, decode, n: int, window: int | None
|
||||
) -> None:
|
||||
shape = (-1, self.num_heads, self.head_dim)
|
||||
self._decode_attention(
|
||||
q.view(shape)[:n],
|
||||
self.attn.kv_cache,
|
||||
decode.block_tables,
|
||||
decode.seq_lens,
|
||||
self.impl.sinks,
|
||||
window,
|
||||
self.impl.scale,
|
||||
out.view(shape)[:n],
|
||||
)
|
||||
|
||||
def forward_ll(
|
||||
self,
|
||||
positions: torch.Tensor,
|
||||
hidden_states: torch.Tensor,
|
||||
residual: torch.Tensor | None,
|
||||
input_layernorm: RMSNorm,
|
||||
) -> tuple[torch.Tensor, torch.Tensor] | None:
|
||||
"""Small pure-decode batches: two fused kernels for the residual-add
|
||||
RMSNorm, qkv GEMV, q/k norm, RoPE and KV-cache write, then the Flint
|
||||
decode attention. Returns None to fall back to the regular path."""
|
||||
if hidden_states.shape[0] > LL_MAX_TOKENS:
|
||||
return None
|
||||
k_cache, v_cache, layer_slots = self._cache_views()
|
||||
plan = self._decode_plan()
|
||||
if layer_slots is None or plan is None:
|
||||
return None
|
||||
layer = self.qkv_proj
|
||||
bf16w = layer.weight.dtype == torch.bfloat16
|
||||
q, residual = flint_qkv_fused(
|
||||
hidden_states,
|
||||
residual,
|
||||
input_layernorm.weight,
|
||||
input_layernorm.variance_epsilon,
|
||||
layer.weight,
|
||||
None if bf16w else layer.flint_weight_scale,
|
||||
None if bf16w else layer.weight_global_scale,
|
||||
self.q_norm.weight,
|
||||
self.k_norm.weight,
|
||||
self.q_norm.variance_epsilon,
|
||||
self._cos_sin(),
|
||||
positions,
|
||||
k_cache,
|
||||
v_cache,
|
||||
layer_slots,
|
||||
self.num_heads,
|
||||
self.num_kv_heads,
|
||||
)
|
||||
attn_output = torch.empty_like(q)
|
||||
self._run_decode_attention(q, attn_output, *plan)
|
||||
output, _ = self.o_proj(attn_output)
|
||||
return output, residual
|
||||
|
||||
|
||||
class FlintDecoderLayer(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
config: FlintConfig,
|
||||
cache_config: CacheConfig | None,
|
||||
quant_config: QuantizationConfig | None,
|
||||
prefix: str = "",
|
||||
) -> None:
|
||||
super().__init__()
|
||||
layer_idx = extract_layer_index(prefix)
|
||||
self.self_attn = FlintAttention(
|
||||
config,
|
||||
layer_idx,
|
||||
cache_config,
|
||||
quant_config,
|
||||
prefix=f"{prefix}.self_attn",
|
||||
)
|
||||
self.mlp: FlintMLP | FlintSparseMoeBlock
|
||||
if layer_idx in config.mlp_only_layers:
|
||||
self.mlp = FlintMLP(
|
||||
config.hidden_size,
|
||||
config.intermediate_size,
|
||||
quant_config,
|
||||
prefix=f"{prefix}.mlp",
|
||||
)
|
||||
else:
|
||||
self.mlp = FlintSparseMoeBlock(config, quant_config, prefix=f"{prefix}.mlp")
|
||||
self.input_layernorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
|
||||
self.post_attention_layernorm = RMSNorm(
|
||||
config.hidden_size, eps=config.rms_norm_eps
|
||||
)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
positions: torch.Tensor,
|
||||
hidden_states: torch.Tensor,
|
||||
residual: torch.Tensor | None,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
if self.self_attn.flint_decode:
|
||||
ll = self.self_attn.forward_ll(
|
||||
positions, hidden_states, residual, self.input_layernorm
|
||||
)
|
||||
if ll is not None:
|
||||
return self._post_attention(*ll)
|
||||
if residual is None:
|
||||
residual = hidden_states
|
||||
hidden_states = self.input_layernorm(hidden_states)
|
||||
else:
|
||||
hidden_states, residual = self.input_layernorm(hidden_states, residual)
|
||||
hidden_states = self.self_attn(positions, hidden_states)
|
||||
return self._post_attention(hidden_states, residual)
|
||||
|
||||
def _post_attention(
|
||||
self, hidden_states: torch.Tensor, residual: torch.Tensor
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
if isinstance(self.mlp, FlintSparseMoeBlock):
|
||||
ll = self.mlp.forward_ll(
|
||||
hidden_states, residual, self.post_attention_layernorm
|
||||
)
|
||||
if ll is not None:
|
||||
return ll
|
||||
hidden_states, residual = self.post_attention_layernorm(hidden_states, residual)
|
||||
hidden_states = self.mlp(hidden_states)
|
||||
return hidden_states, residual
|
||||
|
||||
|
||||
class FlintModel(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
vllm_config: VllmConfig,
|
||||
quant_config: QuantizationConfig | None,
|
||||
prefix: str = "",
|
||||
) -> None:
|
||||
super().__init__()
|
||||
config: FlintConfig = vllm_config.model_config.hf_config
|
||||
cache_config = vllm_config.cache_config
|
||||
self.config = config
|
||||
self.embed_tokens = VocabParallelEmbedding(
|
||||
config.vocab_size,
|
||||
config.hidden_size,
|
||||
prefix=f"{prefix}.embed_tokens",
|
||||
)
|
||||
self.start_layer, self.end_layer, self.layers = make_layers(
|
||||
config.num_hidden_layers,
|
||||
lambda prefix: FlintDecoderLayer(
|
||||
config, cache_config, quant_config, prefix=prefix
|
||||
),
|
||||
prefix=f"{prefix}.layers",
|
||||
)
|
||||
self.norm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
|
||||
self.make_empty_intermediate_tensors = make_empty_intermediate_tensors_factory(
|
||||
["hidden_states", "residual"], config.hidden_size
|
||||
)
|
||||
|
||||
def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
|
||||
return self.embed_tokens(input_ids)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
input_ids: torch.Tensor | None,
|
||||
positions: torch.Tensor,
|
||||
intermediate_tensors: IntermediateTensors | None = None,
|
||||
inputs_embeds: torch.Tensor | None = None,
|
||||
) -> torch.Tensor | IntermediateTensors:
|
||||
if intermediate_tensors is not None:
|
||||
hidden_states = intermediate_tensors["hidden_states"]
|
||||
residual = intermediate_tensors["residual"]
|
||||
else:
|
||||
hidden_states = (
|
||||
inputs_embeds
|
||||
if inputs_embeds is not None
|
||||
else self.embed_input_ids(input_ids)
|
||||
)
|
||||
residual = None
|
||||
for layer in self.layers[self.start_layer : self.end_layer]:
|
||||
hidden_states, residual = layer(positions, hidden_states, residual)
|
||||
if self.end_layer != len(self.layers):
|
||||
return IntermediateTensors(
|
||||
{"hidden_states": hidden_states, "residual": residual}
|
||||
)
|
||||
hidden_states, _ = self.norm(hidden_states, residual)
|
||||
return hidden_states
|
||||
|
||||
|
||||
class FlintForCausalLM(nn.Module, SupportsPP):
|
||||
hf_to_vllm_mapper = WeightsMapper(
|
||||
orig_to_new_stacked={
|
||||
".q_proj": (".qkv_proj", "q"),
|
||||
".k_proj": (".qkv_proj", "k"),
|
||||
".v_proj": (".qkv_proj", "v"),
|
||||
".mlp.gate_proj": (".mlp.gate_up_proj", 0),
|
||||
".mlp.up_proj": (".mlp.gate_up_proj", 1),
|
||||
".shared_expert.gate_proj": (".shared_expert.gate_up_proj", 0),
|
||||
".shared_expert.up_proj": (".shared_expert.gate_up_proj", 1),
|
||||
}
|
||||
)
|
||||
packed_modules_mapping = {
|
||||
"qkv_proj": ["q_proj", "k_proj", "v_proj"],
|
||||
"gate_up_proj": ["gate_proj", "up_proj"],
|
||||
}
|
||||
|
||||
def __init__(self, *, vllm_config: VllmConfig, prefix: str = "") -> None:
|
||||
super().__init__()
|
||||
config: FlintConfig = vllm_config.model_config.hf_config
|
||||
self.config = config
|
||||
|
||||
model_config = vllm_config.model_config
|
||||
self.gguf_path = find_gguf_file(
|
||||
model_config.model,
|
||||
model_config.revision,
|
||||
vllm_config.load_config.download_dir,
|
||||
)
|
||||
quant_config = vllm_config.quant_config
|
||||
if self.gguf_path is not None:
|
||||
if quant_config is not None:
|
||||
raise ValueError(
|
||||
"Flint NVFP4 GGUF checkpoints carry their own "
|
||||
"quantization; do not pass --quantization."
|
||||
)
|
||||
quant_config = _nvfp4_gguf_quant_config()
|
||||
vllm_config.quant_config = quant_config
|
||||
# load_weights reads the GGUF itself and never consumes the default
|
||||
# loader's (lazy) iterator; the pattern just names the right file.
|
||||
self.allow_patterns_overrides = ["*.gguf"]
|
||||
if quant_config is not None:
|
||||
quant_config.packed_modules_mapping = self.packed_modules_mapping
|
||||
self.quant_config = quant_config
|
||||
|
||||
self.model = FlintModel(
|
||||
vllm_config, quant_config, prefix=maybe_prefix(prefix, "model")
|
||||
)
|
||||
self.flint_dense = _flint_kernels(quant_config)
|
||||
if self.flint_dense:
|
||||
flint_linear.install_flint_dense_kernels(self.model)
|
||||
self.lm_head = ParallelLMHead(
|
||||
config.vocab_size,
|
||||
config.hidden_size,
|
||||
quant_config=quant_config,
|
||||
prefix=maybe_prefix(prefix, "lm_head"),
|
||||
)
|
||||
self.logits_processor = LogitsProcessor(config.vocab_size)
|
||||
self.make_empty_intermediate_tensors = (
|
||||
self.model.make_empty_intermediate_tensors
|
||||
)
|
||||
|
||||
def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
|
||||
return self.model.embed_input_ids(input_ids)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
input_ids: torch.Tensor | None,
|
||||
positions: torch.Tensor,
|
||||
intermediate_tensors: IntermediateTensors | None = None,
|
||||
inputs_embeds: torch.Tensor | None = None,
|
||||
) -> torch.Tensor | IntermediateTensors:
|
||||
return self.model(input_ids, positions, intermediate_tensors, inputs_embeds)
|
||||
|
||||
def compute_logits(self, hidden_states: torch.Tensor) -> torch.Tensor | None:
|
||||
return self.logits_processor(self.lm_head, hidden_states)
|
||||
|
||||
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
|
||||
if self.gguf_path is not None:
|
||||
device = next(self.parameters()).device
|
||||
weights = self._apply_dequantized_scales(
|
||||
gguf_nvfp4_weights_iterator(self.gguf_path, device, _GGUF_DEQUANTIZED),
|
||||
)
|
||||
if self.flint_dense:
|
||||
weights = flint_linear.check_merged_global_scales(weights)
|
||||
loader = AutoWeightsLoader(self)
|
||||
return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
|
||||
|
||||
def _apply_dequantized_scales(
|
||||
self, weights: Iterable[tuple[str, torch.Tensor]]
|
||||
) -> Iterable[tuple[str, torch.Tensor]]:
|
||||
for name, tensor in weights:
|
||||
if name == "lm_head.weight_scale_2":
|
||||
self.logits_processor.scale = float(tensor)
|
||||
else:
|
||||
yield name, tensor
|
||||
@@ -0,0 +1,2 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
@@ -0,0 +1,428 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""CuTe DSL device primitives for Flint's small-M BF16 x NVFP4/BF16 GEMV.
|
||||
|
||||
Shared with other Flint kernels; keep signatures stable. A-fragment layout
|
||||
(``mma.sync`` m16n8k16, swap-AB: weight rows are the MMA M dimension): lane
|
||||
``(g, t) = (lane // 4, lane % 4)`` of k-tile ``j`` within a 64-wide K chunk
|
||||
covers ``k = 64c + 16t + 4j + {0..3}`` of rows ``g`` and ``g + 8``.
|
||||
"""
|
||||
|
||||
import cutlass
|
||||
import cutlass.cute as cute
|
||||
from cutlass import Float32, Int32, Int64, Uint32
|
||||
from cutlass._mlir.dialects import llvm
|
||||
from cutlass.cutlass_dsl import dsl_user_op
|
||||
|
||||
|
||||
def ptx(code, constraints, operands, results=None, *, effects=True, loc=None, ip=None):
|
||||
if results is None:
|
||||
ty = None
|
||||
elif isinstance(results, tuple):
|
||||
ty = llvm.StructType.get_literal([r.mlir_type for r in results])
|
||||
else:
|
||||
ty = results.mlir_type
|
||||
value = llvm.inline_asm(
|
||||
ty,
|
||||
[op.ir_value(loc=loc, ip=ip) for op in operands],
|
||||
code,
|
||||
constraints,
|
||||
has_side_effects=effects,
|
||||
is_align_stack=False,
|
||||
asm_dialect=llvm.AsmDialect.AD_ATT,
|
||||
loc=loc,
|
||||
ip=ip,
|
||||
)
|
||||
if results is None:
|
||||
return None
|
||||
if isinstance(results, tuple):
|
||||
return tuple(
|
||||
r(llvm.extractvalue(r.mlir_type, value, [i], loc=loc, ip=ip))
|
||||
for i, r in enumerate(results)
|
||||
)
|
||||
return results(value)
|
||||
|
||||
|
||||
@dsl_user_op
|
||||
def smem_addr(ptr, *, loc=None, ip=None):
|
||||
return Uint32(ptr.toint(loc=loc, ip=ip))
|
||||
|
||||
|
||||
# Loads are volatile so that every weight load is issued up front, in
|
||||
# program order, before the dependent math.
|
||||
@dsl_user_op
|
||||
def ld_w8(addr: Int64, *, loc=None, ip=None):
|
||||
return list(
|
||||
ptx(
|
||||
"ld.global.nc.L1::no_allocate.v2.u32 {$0, $1}, [$2];",
|
||||
"=r,=r,l",
|
||||
[addr],
|
||||
(Uint32,) * 2,
|
||||
loc=loc,
|
||||
ip=ip,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@dsl_user_op
|
||||
def ld_w8p(addr: Int64, *, loc=None, ip=None):
|
||||
# ``ld_w8`` with a 128-byte L2 prefetch: a lane's later chunks of the same
|
||||
# row follow in 32-byte steps.
|
||||
return list(
|
||||
ptx(
|
||||
"ld.global.nc.L1::no_allocate.L2::128B.v2.u32 {$0, $1}, [$2];",
|
||||
"=r,=r,l",
|
||||
[addr],
|
||||
(Uint32,) * 2,
|
||||
loc=loc,
|
||||
ip=ip,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@dsl_user_op
|
||||
def ld_w16(addr: Int64, *, loc=None, ip=None):
|
||||
return list(
|
||||
ptx(
|
||||
"ld.global.nc.L1::no_allocate.v4.u32 {$0, $1, $2, $3}, [$4];",
|
||||
"=r,=r,=r,=r,l",
|
||||
[addr],
|
||||
(Uint32,) * 4,
|
||||
loc=loc,
|
||||
ip=ip,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@dsl_user_op
|
||||
def ld_u8(addr: Int64, *, loc=None, ip=None):
|
||||
return ptx(
|
||||
"{\n.reg .b16 h;\nld.global.nc.u8 h, [$1];\ncvt.u32.u16 $0, h;\n}",
|
||||
"=r,l",
|
||||
[addr],
|
||||
Uint32,
|
||||
loc=loc,
|
||||
ip=ip,
|
||||
)
|
||||
|
||||
|
||||
@dsl_user_op
|
||||
def ld_x16(addr: Int64, pred: Int32, *, loc=None, ip=None):
|
||||
# 16 bf16 activations, or zeros for padded tokens.
|
||||
return list(
|
||||
ptx(
|
||||
"{\n.reg .pred p;\nsetp.ne.b32 p, $9, 0;\n"
|
||||
"mov.b32 $0, 0; mov.b32 $1, 0; mov.b32 $2, 0; mov.b32 $3, 0;\n"
|
||||
"mov.b32 $4, 0; mov.b32 $5, 0; mov.b32 $6, 0; mov.b32 $7, 0;\n"
|
||||
"@p ld.global.v4.u32 {$0, $1, $2, $3}, [$8];\n"
|
||||
"@p ld.global.v4.u32 {$4, $5, $6, $7}, [$8+16];\n}",
|
||||
"=r,=r,=r,=r,=r,=r,=r,=r,l,r",
|
||||
[addr, pred],
|
||||
(Uint32,) * 8,
|
||||
loc=loc,
|
||||
ip=ip,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@dsl_user_op
|
||||
def scale_pair(sc: Uint32, *, loc=None, ip=None):
|
||||
# E4M3 byte -> BF16x2 (s, s); exact.
|
||||
return ptx(
|
||||
"{\n.reg .b16 h;\n.reg .b8 b;\ncvt.u16.u32 h, $1;\n"
|
||||
"mov.b16 {b, _}, h;\nmov.b16 h, {b, b};\n"
|
||||
"cvt.rn.bf16x2.e4m3x2 $0, h;\n}",
|
||||
"=r,r",
|
||||
[sc],
|
||||
Uint32,
|
||||
effects=False,
|
||||
loc=loc,
|
||||
ip=ip,
|
||||
)
|
||||
|
||||
|
||||
@dsl_user_op
|
||||
def decode(
|
||||
lo: Uint32,
|
||||
hi: Uint32,
|
||||
j: cutlass.Constexpr,
|
||||
sg: Uint32,
|
||||
sg8: Uint32,
|
||||
*,
|
||||
loc=None,
|
||||
ip=None,
|
||||
):
|
||||
# A fragment of k-tile j: bytes (2j, 2j+1) % 4 of the row-g word `lo`
|
||||
# and the row-(g+8) word `hi`, scaled exactly in BF16.
|
||||
b = (2 * j) % 4
|
||||
return list(
|
||||
ptx(
|
||||
"{\n.reg .b8 b0, b1, b2, b3, c0, c1, c2, c3;\n"
|
||||
"mov.b32 {b0, b1, b2, b3}, $4;\nmov.b32 {c0, c1, c2, c3}, $5;\n"
|
||||
f"cvt.rn.bf16x2.e2m1x2 $0, b{b};\n"
|
||||
f"cvt.rn.bf16x2.e2m1x2 $1, c{b};\n"
|
||||
f"cvt.rn.bf16x2.e2m1x2 $2, b{b + 1};\n"
|
||||
f"cvt.rn.bf16x2.e2m1x2 $3, c{b + 1};\n"
|
||||
"mul.rn.bf16x2 $0, $0, $6;\nmul.rn.bf16x2 $1, $1, $7;\n"
|
||||
"mul.rn.bf16x2 $2, $2, $6;\nmul.rn.bf16x2 $3, $3, $7;\n}",
|
||||
"=r,=r,=r,=r,r,r,r,r",
|
||||
[lo, hi, sg, sg8],
|
||||
(Uint32,) * 4,
|
||||
effects=False,
|
||||
loc=loc,
|
||||
ip=ip,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@dsl_user_op
|
||||
def mma(c, a, b0: Uint32, b1: Uint32, *, loc=None, ip=None):
|
||||
return list(
|
||||
ptx(
|
||||
"mma.sync.aligned.m16n8k16.row.col.f32.bf16.bf16.f32 "
|
||||
"{$0, $1, $2, $3}, {$8, $9, $10, $11}, {$12, $13}, {$4, $5, $6, $7};",
|
||||
"=f,=f,=f,=f,f,f,f,f,r,r,r,r,r,r",
|
||||
[*c, *a, b0, b1],
|
||||
(Float32,) * 4,
|
||||
effects=False,
|
||||
loc=loc,
|
||||
ip=ip,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@dsl_user_op
|
||||
def st_out(addr: Int64, v: Float32, pred: Int32, *, loc=None, ip=None):
|
||||
ptx(
|
||||
"{\n.reg .pred p;\n.reg .b16 h;\nsetp.ne.b32 p, $2, 0;\n"
|
||||
"cvt.rn.bf16.f32 h, $1;\n@p st.global.b16 [$0], h;\n}",
|
||||
"l,f,r",
|
||||
[addr, v, pred],
|
||||
loc=loc,
|
||||
ip=ip,
|
||||
)
|
||||
|
||||
|
||||
@dsl_user_op
|
||||
def st_dsmem_f32(smem_ptr, v: Float32, *, loc=None, ip=None):
|
||||
remote = cute.arch.map_dsmem_ptr(smem_ptr, Int32(0), loc=loc, ip=ip)
|
||||
addr = Int32(remote.toint(loc=loc, ip=ip))
|
||||
ptx("st.shared::cluster.f32 [$0], $1;", "r,f", [addr, v], loc=loc, ip=ip)
|
||||
|
||||
|
||||
@dsl_user_op
|
||||
def silu_mul(g: Float32, u: Float32, *, loc=None, ip=None):
|
||||
# g / (1 + expf(-g)) * u in fp32 (rounded once by the caller's store), with
|
||||
# CUDA's precise expf (libdevice __nv_expf) and an IEEE division.
|
||||
return ptx(
|
||||
"{\n.reg .b32 x, t, j, f, e, sc, d, a;\n"
|
||||
"neg.f32 x, $1;\n"
|
||||
"fma.rn.ftz.f32 t, x, 0f3BBB989D, 0f3F000000;\n"
|
||||
"cvt.ftz.sat.f32.f32 t, t;\n"
|
||||
"fma.rm.ftz.f32 j, t, 0f437C0000, 0f4B400001;\n"
|
||||
"add.f32 f, j, 0fCB40007F;\n"
|
||||
"neg.f32 f, f;\n"
|
||||
"fma.rn.ftz.f32 f, x, 0f3FB8AA3B, f;\n"
|
||||
"fma.rn.ftz.f32 f, x, 0f32A57060, f;\n"
|
||||
"shl.b32 sc, j, 23;\n"
|
||||
"ex2.approx.ftz.f32 e, f;\n"
|
||||
"fma.rn.f32 d, e, sc, 0f3F800000;\n"
|
||||
"div.rn.f32 a, $1, d;\n"
|
||||
"mul.f32 $0, a, $2;\n}",
|
||||
"=f,f,f",
|
||||
[g, u],
|
||||
Float32,
|
||||
effects=False,
|
||||
loc=loc,
|
||||
ip=ip,
|
||||
)
|
||||
|
||||
|
||||
@dsl_user_op
|
||||
def round_bf16(v: Float32, *, loc=None, ip=None):
|
||||
return ptx(
|
||||
"{\n.reg .b16 h;\ncvt.rn.bf16.f32 h, $1;\ncvt.f32.bf16 $0, h;\n}",
|
||||
"=f,f",
|
||||
[v],
|
||||
Float32,
|
||||
effects=False,
|
||||
loc=loc,
|
||||
ip=ip,
|
||||
)
|
||||
|
||||
|
||||
@dsl_user_op
|
||||
def sts_f32(addr, a, *, loc=None, ip=None):
|
||||
ptx(
|
||||
"st.shared.f32 [$0], $1;",
|
||||
"r,f,~{memory}",
|
||||
[Uint32(addr), Float32(a)],
|
||||
loc=loc,
|
||||
ip=ip,
|
||||
)
|
||||
|
||||
|
||||
@dsl_user_op
|
||||
def lds_f32(addr, *, loc=None, ip=None):
|
||||
return ptx(
|
||||
"ld.shared.f32 $0, [$1];",
|
||||
"=f,r,~{memory}",
|
||||
[Uint32(addr)],
|
||||
Float32,
|
||||
loc=loc,
|
||||
ip=ip,
|
||||
)
|
||||
|
||||
|
||||
@dsl_user_op
|
||||
def stg_bf16(addr, a, *, loc=None, ip=None):
|
||||
ptx(
|
||||
"{ .reg .b16 h; cvt.rn.bf16.f32 h, $1; st.global.b16 [$0], h; }",
|
||||
"l,f,~{memory}",
|
||||
[Int64(addr), Float32(a)],
|
||||
loc=loc,
|
||||
ip=ip,
|
||||
)
|
||||
|
||||
|
||||
@dsl_user_op
|
||||
def stg_u32x4(addr, a, b, c, d, *, loc=None, ip=None):
|
||||
ptx(
|
||||
"st.global.v4.b32 [$0], {$1,$2,$3,$4};",
|
||||
"l,r,r,r,r,~{memory}",
|
||||
[Int64(addr), Uint32(a), Uint32(b), Uint32(c), Uint32(d)],
|
||||
loc=loc,
|
||||
ip=ip,
|
||||
)
|
||||
|
||||
|
||||
@dsl_user_op
|
||||
def bf16x2(lo, hi, *, loc=None, ip=None):
|
||||
return ptx(
|
||||
"cvt.rn.bf16x2.f32 $0, $1, $2;",
|
||||
"=r,f,f",
|
||||
[Float32(hi), Float32(lo)],
|
||||
Uint32,
|
||||
effects=False,
|
||||
loc=loc,
|
||||
ip=ip,
|
||||
)
|
||||
|
||||
|
||||
@dsl_user_op
|
||||
def fma(a, b, c, *, loc=None, ip=None):
|
||||
return ptx(
|
||||
"fma.rn.f32 $0, $1, $2, $3;",
|
||||
"=f,f,f,f",
|
||||
[Float32(a), Float32(b), Float32(c)],
|
||||
Float32,
|
||||
effects=False,
|
||||
loc=loc,
|
||||
ip=ip,
|
||||
)
|
||||
|
||||
|
||||
@cute.jit
|
||||
def load_rows(
|
||||
w_base,
|
||||
s_base,
|
||||
row,
|
||||
KC: cutlass.Constexpr,
|
||||
CPW: cutlass.Constexpr,
|
||||
c_begin,
|
||||
t,
|
||||
qs,
|
||||
ss,
|
||||
q_off: cutlass.Constexpr,
|
||||
s_off: cutlass.Constexpr,
|
||||
PREFETCH: cutlass.Constexpr = False,
|
||||
):
|
||||
"""NVFP4 weight words / scale bytes of rows (row, row + 8) for CPW chunks
|
||||
from ``c_begin`` (ModelOpt weight, 128x4-swizzled scales)."""
|
||||
kb = KC * 32
|
||||
wbase = w_base + Int64(row) * kb + c_begin * 32 + t * 8
|
||||
row8 = row + 8
|
||||
sb_g = (
|
||||
s_base
|
||||
+ Int64(row // 128) * (KC * 512)
|
||||
+ (row % 32) * 16
|
||||
+ ((row % 128) // 32) * 4
|
||||
+ t
|
||||
+ c_begin * 512
|
||||
)
|
||||
sb_g8 = (
|
||||
s_base
|
||||
+ Int64(row8 // 128) * (KC * 512)
|
||||
+ (row8 % 32) * 16
|
||||
+ ((row8 % 128) // 32) * 4
|
||||
+ t
|
||||
+ c_begin * 512
|
||||
)
|
||||
for c in cutlass.range_constexpr(CPW):
|
||||
if cutlass.const_expr(PREFETCH):
|
||||
qa = ld_w8p(wbase + c * 32)
|
||||
qb = ld_w8p(wbase + 8 * kb + c * 32)
|
||||
else:
|
||||
qa = ld_w8(wbase + c * 32)
|
||||
qb = ld_w8(wbase + 8 * kb + c * 32)
|
||||
q0 = q_off + c * 4
|
||||
qs[q0 + 0] = qa[0]
|
||||
qs[q0 + 1] = qa[1]
|
||||
qs[q0 + 2] = qb[0]
|
||||
qs[q0 + 3] = qb[1]
|
||||
ss[s_off + c * 2] = ld_u8(sb_g + c * 512)
|
||||
ss[s_off + c * 2 + 1] = ld_u8(sb_g8 + c * 512)
|
||||
|
||||
|
||||
def alloc_reduce(smem, NA, WARPS, SPLIT):
|
||||
red = smem.allocate_tensor(
|
||||
Float32,
|
||||
cute.make_layout((WARPS, NA, 32), stride=(NA * 32, 32, 1)),
|
||||
byte_alignment=16,
|
||||
)
|
||||
part = smem.allocate_tensor(
|
||||
Float32,
|
||||
cute.make_layout((SPLIT, NA, 32), stride=(NA * 32, 32, 1)),
|
||||
byte_alignment=16,
|
||||
)
|
||||
return red, part
|
||||
|
||||
|
||||
@cute.jit
|
||||
def cta_reduce(
|
||||
acc,
|
||||
NA: cutlass.Constexpr,
|
||||
WARPS: cutlass.Constexpr,
|
||||
SPLIT: cutlass.Constexpr,
|
||||
warp,
|
||||
lane,
|
||||
rank,
|
||||
red,
|
||||
part,
|
||||
):
|
||||
"""Sum each lane's NA accumulators over the CTA's warps and the cluster's
|
||||
CTAs (fixed order) into warp 0 of cluster rank 0. With SPLIT > 1 the
|
||||
caller must have issued ``cluster_arrive_relaxed``."""
|
||||
for i in cutlass.range_constexpr(NA):
|
||||
red[warp, i, lane] = acc[i]
|
||||
cute.arch.sync_threads()
|
||||
if cutlass.const_expr(SPLIT > 1):
|
||||
cute.arch.cluster_wait()
|
||||
if warp == 0:
|
||||
for i in cutlass.range_constexpr(NA):
|
||||
v = acc[i]
|
||||
for w2 in cutlass.range_constexpr(1, WARPS):
|
||||
v = v + red[w2, i, lane]
|
||||
acc[i] = v
|
||||
if cutlass.const_expr(SPLIT > 1):
|
||||
for i in cutlass.range_constexpr(NA):
|
||||
st_dsmem_f32(part.iterator + (rank * NA + i) * 32 + lane, acc[i])
|
||||
if cutlass.const_expr(SPLIT > 1):
|
||||
cute.arch.cluster_arrive()
|
||||
cute.arch.cluster_wait()
|
||||
if warp == 0 and rank == 0:
|
||||
for i in cutlass.range_constexpr(NA):
|
||||
v = part[0, i, lane]
|
||||
for r2 in cutlass.range_constexpr(1, SPLIT):
|
||||
v = v + part[r2, i, lane]
|
||||
acc[i] = v
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,253 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Flint dense linears with a small-M CuTe GEMV fast path.
|
||||
|
||||
NVFP4 (W4A16): the GEMV (``nvfp4_gemv``) reads the very tensors the FlashInfer
|
||||
CuTe-DSL kernel was prepared with (ModelOpt-packed weight, 128x4-swizzled
|
||||
scales), so dispatching per token count costs no extra weight memory; larger
|
||||
M stays on FlashInfer. BF16: the same GEMV kernel on BF16 weights where it
|
||||
beats cuBLAS, cuBLAS otherwise.
|
||||
"""
|
||||
|
||||
from collections.abc import Iterable, Iterator
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
from torch import nn
|
||||
|
||||
from vllm.logger import init_logger
|
||||
from vllm.model_executor.kernels.linear.nvfp4.flashinfer import (
|
||||
FlashInferCuteDslNvFp4W4A16LinearKernel,
|
||||
)
|
||||
from vllm.model_executor.layers.linear import LinearBase, UnquantizedLinearMethod
|
||||
|
||||
from . import nvfp4_gemv
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
def _swizzled_scale_buffer(weight_scale: torch.Tensor, n: int, k: int) -> torch.Tensor:
|
||||
"""The flat 128x4-swizzled scale buffer behind FlashInfer's strided view of
|
||||
it (no copy)."""
|
||||
nbytes = (n + 127) // 128 * 128 * ((k // 16 + 3) // 4 * 4)
|
||||
storage = weight_scale.untyped_storage()
|
||||
assert (
|
||||
weight_scale.dtype == torch.float8_e4m3fn
|
||||
and weight_scale.storage_offset() == 0
|
||||
and storage.nbytes() == nbytes
|
||||
), "FlashInfer NVFP4 scales are not the flat 128x4-swizzled buffer"
|
||||
return torch.empty(0, dtype=torch.uint8, device=weight_scale.device).set_(
|
||||
storage, 0, (nbytes,), (1,)
|
||||
)
|
||||
|
||||
|
||||
class FlintNvFp4LinearKernel(FlashInferCuteDslNvFp4W4A16LinearKernel):
|
||||
"""FlashInfer CuTe-DSL W4A16 GEMM with a small-M CuTe GEMV fast path."""
|
||||
|
||||
def process_weights_after_loading(self, layer: nn.Module) -> None:
|
||||
super().process_weights_after_loading(layer)
|
||||
n, k = layer.output_size_per_partition, layer.input_size_per_partition
|
||||
assert (
|
||||
getattr(layer, "weights_padding_cols", 0) == 0
|
||||
and layer.weight.dtype == torch.uint8
|
||||
and tuple(layer.weight.shape) == (n, k // 2)
|
||||
and layer.weight.is_contiguous()
|
||||
), "FlashInfer NVFP4 weight is not in the GEMV layout"
|
||||
layer.flint_weight_scale = _swizzled_scale_buffer(layer.weight_scale, n, k)
|
||||
# Host copy of the global scale for the large-M cuBLAS alpha.
|
||||
layer.flint_global_scale = float(layer.weight_global_scale.item())
|
||||
layer.flint_bf16 = False
|
||||
layer.flint_pdl = True
|
||||
layer.flint_nk = (n, k)
|
||||
_setup_gemv(layer, n, k, True, bf16=False)
|
||||
|
||||
def apply_weights(
|
||||
self,
|
||||
layer: nn.Module,
|
||||
x: torch.Tensor,
|
||||
bias: torch.Tensor | None = None,
|
||||
) -> torch.Tensor:
|
||||
assert bias is None
|
||||
x_2d = x.reshape(-1, x.shape[-1])
|
||||
if x_2d.shape[0] >= _DEQUANT_MIN_M:
|
||||
out = _dequant_gemm(layer, x_2d)
|
||||
return out.view(*x.shape[:-1], out.shape[-1])
|
||||
if _use_gemv(layer, x_2d, silu_mul=False):
|
||||
out = nvfp4_gemv.nvfp4_gemv(
|
||||
x_2d,
|
||||
layer.weight,
|
||||
layer.flint_weight_scale,
|
||||
layer.weight_global_scale,
|
||||
)
|
||||
return out.view(*x.shape[:-1], out.shape[-1])
|
||||
return super().apply_weights(layer, x, bias)
|
||||
|
||||
|
||||
class FlintBf16LinearMethod(UnquantizedLinearMethod):
|
||||
"""BF16 linear: CuTe GEMV for small M, cuBLAS otherwise."""
|
||||
|
||||
def __init__(self, pdl: bool = True) -> None:
|
||||
super().__init__()
|
||||
self.pdl = pdl
|
||||
|
||||
def process_weights_after_loading(self, layer: nn.Module) -> None:
|
||||
super().process_weights_after_loading(layer)
|
||||
w = layer.weight
|
||||
n, k = w.shape
|
||||
assert w.dtype == torch.bfloat16 and w.is_contiguous()
|
||||
layer.flint_bf16 = True
|
||||
layer.flint_pdl = self.pdl
|
||||
layer.flint_nk = (n, k)
|
||||
_setup_gemv(layer, n, k, self.pdl, bf16=True)
|
||||
|
||||
def apply(
|
||||
self,
|
||||
layer: nn.Module,
|
||||
x: torch.Tensor,
|
||||
bias: torch.Tensor | None = None,
|
||||
) -> torch.Tensor:
|
||||
assert bias is None
|
||||
x_2d = x.reshape(-1, x.shape[-1])
|
||||
if _use_gemv(layer, x_2d, silu_mul=False):
|
||||
out = nvfp4_gemv.bf16_gemv(x_2d, layer.weight, pdl=self.pdl)
|
||||
return out.view(*x.shape[:-1], out.shape[-1])
|
||||
return super().apply(layer, x, bias)
|
||||
|
||||
|
||||
# Token counts from which exact BF16 dequantization + cuBLAS beats FlashInfer
|
||||
# CuTe-DSL in the full model (GB200). At 8192 tokens it wins in isolation
|
||||
# (o_proj 107 -> 86 us) but loses in-model (dequant ~9 us under contention).
|
||||
_DEQUANT_MIN_M = 16384
|
||||
|
||||
|
||||
def _dequant_gemm(layer: nn.Module, x: torch.Tensor) -> torch.Tensor:
|
||||
"""``bf16(fp32(x @ bf16(e2m1 * e4m3).T) * global)`` via cuBLAS: the global
|
||||
scale is the GEMM alpha, applied to the FP32 accumulator."""
|
||||
w = nvfp4_gemv.dequantize_rows(layer.weight, layer.flint_weight_scale)
|
||||
out = torch.empty(x.shape[0], w.shape[0], dtype=torch.bfloat16, device=x.device)
|
||||
matmul = torch.backends.cuda.matmul
|
||||
prev = matmul.allow_bf16_reduced_precision_reduction
|
||||
matmul.allow_bf16_reduced_precision_reduction = False # FP32 split-K sums
|
||||
try:
|
||||
torch.addmm(out, x, w.t(), beta=0, alpha=layer.flint_global_scale, out=out)
|
||||
finally:
|
||||
matmul.allow_bf16_reduced_precision_reduction = prev
|
||||
return out
|
||||
|
||||
|
||||
def _setup_gemv(layer: nn.Module, n: int, k: int, pdl: bool, bf16: bool) -> None:
|
||||
"""Enable (and compile) the GEMV where its tuned table lists the shape."""
|
||||
layer.flint_gemv = nvfp4_gemv.tuned(n, k, bf16=bf16)
|
||||
layer.flint_gemv_silu_mul = layer.output_partition_sizes == [
|
||||
n // 2
|
||||
] * 2 and nvfp4_gemv.tuned(n, k, silu_mul=True, bf16=bf16)
|
||||
for silu_mul in (False, True):
|
||||
if layer.flint_gemv_silu_mul if silu_mul else layer.flint_gemv:
|
||||
nvfp4_gemv.warmup(n, k, silu_mul=silu_mul, pdl=pdl, bf16=bf16)
|
||||
logger.info_once(
|
||||
"Flint dense %s%s (N=%d, K=%d) -> Flint CuTe GEMV for small M",
|
||||
"BF16" if bf16 else "W4A16",
|
||||
" + SiLU-and-mul" if silu_mul else "",
|
||||
n,
|
||||
k,
|
||||
)
|
||||
|
||||
|
||||
def _gemv_input_ok(x: torch.Tensor) -> bool:
|
||||
# 16-byte vector loads of activation rows.
|
||||
return x.stride(-1) == 1 and x.stride(0) % 8 == 0 and x.data_ptr() % 16 == 0
|
||||
|
||||
|
||||
def _use_gemv(layer: nn.Module, x: torch.Tensor, silu_mul: bool) -> bool:
|
||||
"""Whether the tuned dispatch table picks the GEMV for this M."""
|
||||
flag = layer.flint_gemv_silu_mul if silu_mul else layer.flint_gemv
|
||||
if not flag or x.shape[0] == 0:
|
||||
return False
|
||||
n, k = layer.flint_nk
|
||||
if nvfp4_gemv.pick_nt(n, k, x.shape[0], silu_mul, layer.flint_bf16) is None:
|
||||
return False
|
||||
assert x.dtype == torch.bfloat16 and _gemv_input_ok(x)
|
||||
return True
|
||||
|
||||
|
||||
def gate_up_silu_mul(layer: nn.Module, x: torch.Tensor) -> torch.Tensor | None:
|
||||
"""``silu_and_mul(layer(x))`` in one GEMV for small M, else None."""
|
||||
if not (
|
||||
getattr(layer, "flint_gemv_silu_mul", False)
|
||||
and x.dim() == 2
|
||||
and _use_gemv(layer, x, silu_mul=True)
|
||||
):
|
||||
return None
|
||||
if layer.flint_bf16:
|
||||
return nvfp4_gemv.bf16_gemv(x, layer.weight, silu_mul=True, pdl=layer.flint_pdl)
|
||||
return nvfp4_gemv.nvfp4_gemv(
|
||||
x,
|
||||
layer.weight,
|
||||
layer.flint_weight_scale,
|
||||
layer.weight_global_scale,
|
||||
silu_mul=True,
|
||||
pdl=layer.flint_pdl,
|
||||
)
|
||||
|
||||
|
||||
_DENSE_SUFFIXES = ("qkv_proj", "o_proj", "gate_up_proj", "down_proj")
|
||||
|
||||
|
||||
# BF16 layers launched without PDL: the 4x larger weight prefetch of a BF16
|
||||
# o_proj slows the attention tail (+0.18 ms per d1 forward with PDL).
|
||||
_BF16_NO_PDL_SUFFIXES = ("o_proj",)
|
||||
|
||||
|
||||
def install_flint_dense_kernels(model: nn.Module) -> None:
|
||||
"""Route the dense linears of ``model`` through the Flint GEMV (launched
|
||||
with PDL except the BF16 ``_BF16_NO_PDL_SUFFIXES``): NVFP4 linears get
|
||||
``FlintNvFp4LinearKernel``, unquantized BF16 projections
|
||||
(``_DENSE_SUFFIXES``) ``FlintBf16LinearMethod``. Must run before weights
|
||||
are processed after loading."""
|
||||
count = 0
|
||||
for module in model.modules():
|
||||
if not isinstance(module, LinearBase):
|
||||
continue
|
||||
method: Any = getattr(module, "quant_method", None)
|
||||
kernel = getattr(method, "kernel", None)
|
||||
if type(kernel) is FlashInferCuteDslNvFp4W4A16LinearKernel:
|
||||
method.kernel = FlintNvFp4LinearKernel(kernel.config)
|
||||
count += 1
|
||||
elif type(method) is UnquantizedLinearMethod and module.prefix.endswith(
|
||||
_DENSE_SUFFIXES
|
||||
):
|
||||
pdl = not module.prefix.endswith(_BF16_NO_PDL_SUFFIXES)
|
||||
module.quant_method = FlintBf16LinearMethod(pdl)
|
||||
count += 1
|
||||
logger.info("Flint dense GEMV installed on %d linear layers", count)
|
||||
|
||||
|
||||
_MERGED_SHARDS = {
|
||||
".q_proj.": ".qkv_proj.",
|
||||
".k_proj.": ".qkv_proj.",
|
||||
".v_proj.": ".qkv_proj.",
|
||||
".gate_proj.": ".gate_up_proj.",
|
||||
".up_proj.": ".gate_up_proj.",
|
||||
}
|
||||
|
||||
|
||||
def check_merged_global_scales(
|
||||
weights: Iterable[tuple[str, torch.Tensor]],
|
||||
) -> Iterator[tuple[str, torch.Tensor]]:
|
||||
"""Pass checkpoint tensors through, raising if the NVFP4 global scales of
|
||||
shards fused into one linear (q/k/v, gate/up) differ: the fused layer keeps
|
||||
a single global scale."""
|
||||
seen: dict[str, torch.Tensor] = {}
|
||||
for name, tensor in weights:
|
||||
if name.endswith(".weight_scale_2"):
|
||||
for shard, merged in _MERGED_SHARDS.items():
|
||||
if shard in name:
|
||||
key = name.replace(shard, merged)
|
||||
ref = seen.setdefault(key, tensor)
|
||||
if not torch.equal(ref.reshape(()), tensor.reshape(())):
|
||||
raise ValueError(
|
||||
f"{name}: global scale {tensor.item()} differs from "
|
||||
f"the other shards of {key} ({ref.item()})"
|
||||
)
|
||||
break
|
||||
yield name, tensor
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,210 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""FlashInfer's SM100 NVFP4-weight / BF16-activation grouped MoE GEMMs.
|
||||
|
||||
The FlashInfer kernel decodes E2M1 * E4M3 weight tiles to BF16 and runs BF16
|
||||
``tcgen05`` MMAs with fp32 accumulation and the per-expert fp32 global scale
|
||||
applied to the accumulator. Its SwiGLU epilogue keeps gate/up in fp32 and
|
||||
rounds ``silu(g) * u`` once (sigmoid via ``ex2.approx`` + ``rcp.approx``), the
|
||||
rounding rule every Flint MoE path follows.
|
||||
"""
|
||||
|
||||
from functools import cache
|
||||
|
||||
import torch
|
||||
|
||||
|
||||
# ((mma_m, route_tile, mma_k), cluster_mn, raster_along_m) for GEMM1 / GEMM2,
|
||||
# tuned on GB200 for E=256, H=2048, I=512, top-6.
|
||||
def _tactic(num_tokens: int):
|
||||
if num_tokens >= 4096:
|
||||
return (((256, 128, 256), (2, 1), True), ((128, 128, 256), (2, 1), True))
|
||||
if num_tokens >= 2048:
|
||||
return (((256, 128, 256), (2, 1), True), ((256, 128, 256), (2, 1), True))
|
||||
if num_tokens >= 512:
|
||||
return (((128, 32, 256), (2, 1), True), ((128, 32, 256), (2, 1), True))
|
||||
return (((128, 8, 256), (2, 1), True), ((128, 8, 256), (2, 1), True))
|
||||
|
||||
|
||||
def grouped_moe(
|
||||
x: torch.Tensor,
|
||||
topk_ids: torch.Tensor,
|
||||
topk_weights: torch.Tensor,
|
||||
w1: torch.Tensor,
|
||||
w1_sf: torch.Tensor,
|
||||
w1_alpha: torch.Tensor,
|
||||
w2: torch.Tensor,
|
||||
w2_sf: torch.Tensor,
|
||||
w2_alpha: torch.Tensor,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Routed expert outputs in permuted order (unweighted, BF16).
|
||||
|
||||
Returns (y [route_slots, H], expanded_idx_to_permuted_idx [M, TOPK]); the
|
||||
caller does the weighted top-k reduction.
|
||||
"""
|
||||
from flashinfer.fused_moe.cute_dsl.blackwell import moe_w4a16 as lmod
|
||||
from flashinfer.fused_moe.cute_dsl.moe_utils import (
|
||||
allocate_moe_sort_buffers,
|
||||
get_max_num_permuted_tokens,
|
||||
moe_permute,
|
||||
moe_sort,
|
||||
)
|
||||
from flashinfer.tllm_enums import (
|
||||
DEFAULT_SWIGLU_ALPHA,
|
||||
DEFAULT_SWIGLU_BETA,
|
||||
DEFAULT_SWIGLU_LIMIT,
|
||||
ActivationType,
|
||||
)
|
||||
|
||||
num_tokens, hidden = x.shape
|
||||
top_k = topk_ids.shape[1]
|
||||
num_experts = w1.shape[0]
|
||||
inter = w2.shape[2] * 2
|
||||
gemm1_tactic, gemm2_tactic = _tactic(num_tokens)
|
||||
route_tile = gemm1_tactic[0][1]
|
||||
route_slots = get_max_num_permuted_tokens(
|
||||
num_tokens, top_k, num_experts, route_tile
|
||||
)
|
||||
sort_bufs = allocate_moe_sort_buffers(
|
||||
num_tokens=num_tokens,
|
||||
num_experts=num_experts,
|
||||
top_k=top_k,
|
||||
num_local_experts=num_experts,
|
||||
tile_tokens_dim=route_tile,
|
||||
device=x.device,
|
||||
)
|
||||
(
|
||||
tile_to_expert,
|
||||
tile_to_mn_limit,
|
||||
expanded_to_permuted,
|
||||
permuted_to_expanded,
|
||||
_,
|
||||
num_tiles,
|
||||
) = moe_sort(
|
||||
token_selected_experts=topk_ids,
|
||||
token_final_scales=topk_weights,
|
||||
num_experts=num_experts,
|
||||
top_k=top_k,
|
||||
local_expert_offset=0,
|
||||
num_local_experts=num_experts,
|
||||
tile_tokens_dim=route_tile,
|
||||
enable_pdl=True,
|
||||
**sort_bufs,
|
||||
)
|
||||
num_route_tiles = route_slots // route_tile
|
||||
tile_to_expert = tile_to_expert[:num_route_tiles]
|
||||
tile_to_mn_limit = tile_to_mn_limit[:num_route_tiles]
|
||||
permuted_to_expanded = permuted_to_expanded[:route_slots]
|
||||
hidden_ws = torch.empty(route_slots, hidden, dtype=x.dtype, device=x.device)
|
||||
inter_ws = torch.empty(route_slots, inter, dtype=x.dtype, device=x.device)
|
||||
moe_permute(
|
||||
input=x,
|
||||
permuted_output=hidden_ws,
|
||||
tile_idx_to_mn_limit=tile_to_mn_limit,
|
||||
permuted_idx_to_expanded_idx=permuted_to_expanded,
|
||||
num_non_exiting_tiles=num_tiles,
|
||||
max_num_permuted_tokens=route_slots,
|
||||
top_k=top_k,
|
||||
tile_size=route_tile,
|
||||
enable_pdl=True,
|
||||
)
|
||||
common = dict(
|
||||
tile_idx_to_expert_idx=tile_to_expert,
|
||||
tile_idx_to_mn_limit=tile_to_mn_limit,
|
||||
num_non_exiting_tiles=num_tiles,
|
||||
num_local_experts=num_experts,
|
||||
swiglu_alpha=DEFAULT_SWIGLU_ALPHA,
|
||||
swiglu_beta=DEFAULT_SWIGLU_BETA,
|
||||
swiglu_limit=DEFAULT_SWIGLU_LIMIT,
|
||||
situ_beta=None,
|
||||
situ_linear_beta=None,
|
||||
use_fused_finalize=False,
|
||||
permuted_idx_to_expanded_idx=None,
|
||||
token_final_scales=None,
|
||||
enable_pdl=True,
|
||||
)
|
||||
lmod._run_grouped_gemm(
|
||||
weight=w1,
|
||||
weight_sf=w1_sf,
|
||||
activations=hidden_ws,
|
||||
alpha=w1_alpha,
|
||||
output=inter_ws,
|
||||
activation_type=ActivationType.Swiglu,
|
||||
tactic=gemm1_tactic,
|
||||
**common,
|
||||
)
|
||||
# GEMM2 writes over the permuted activations, which are dead by now.
|
||||
lmod._run_grouped_gemm(
|
||||
weight=w2,
|
||||
weight_sf=w2_sf,
|
||||
activations=inter_ws,
|
||||
alpha=w2_alpha,
|
||||
output=hidden_ws,
|
||||
activation_type=None,
|
||||
tactic=gemm2_tactic,
|
||||
**common,
|
||||
)
|
||||
return hidden_ws, expanded_to_permuted
|
||||
|
||||
|
||||
def _cdiv(a: int, b: int) -> int:
|
||||
return -(-a // b)
|
||||
|
||||
|
||||
def _compile_key(num_tokens: int, top_k: int, num_experts: int, gemm1_rows: int):
|
||||
"""The parts of the kernel cache keys that vary with num_tokens (tactics
|
||||
and whether GEMM1 uses the CLC scheduler, as in FlashInfer's launcher)."""
|
||||
from flashinfer.cute_dsl.utils import get_max_active_clusters
|
||||
from flashinfer.fused_moe.cute_dsl.moe_utils import get_max_num_permuted_tokens
|
||||
|
||||
gemm1, gemm2 = _tactic(num_tokens)
|
||||
(mma_m, route_tile, _), cluster, _ = gemm1
|
||||
slots = get_max_num_permuted_tokens(num_tokens, top_k, num_experts, route_tile)
|
||||
cta_m = mma_m // (2 if mma_m == 256 else 1)
|
||||
clusters = _cdiv(_cdiv(gemm1_rows, cta_m), cluster[0]) * _cdiv(
|
||||
_cdiv(slots, route_tile), cluster[1]
|
||||
)
|
||||
clc = clusters > get_max_active_clusters(cluster[0] * cluster[1])
|
||||
return gemm1, gemm2, clc
|
||||
|
||||
|
||||
@cache
|
||||
def _variants(top_k, num_experts, gemm1_rows, min_tokens, max_tokens) -> dict:
|
||||
"""Smallest token count of each kernel cache key in [min, max]."""
|
||||
reps: dict = {}
|
||||
for n in range(min_tokens, max_tokens + 1):
|
||||
reps.setdefault(_compile_key(n, top_k, num_experts, gemm1_rows), n)
|
||||
return reps
|
||||
|
||||
|
||||
_WARMED: set[tuple] = set()
|
||||
|
||||
|
||||
def warmup(weights, top_k: int, min_tokens: int, max_tokens: int) -> None:
|
||||
"""Compile every grouped-GEMM kernel variant used for min..max tokens.
|
||||
|
||||
CuTe kernels compile on first use, which must not happen inside CUDA graph
|
||||
capture; the compile caches are process-wide, so each variant runs once.
|
||||
"""
|
||||
num_experts, gemm1_rows = weights.w1.shape[:2]
|
||||
hidden = weights.w2.shape[1]
|
||||
reps = _variants(top_k, num_experts, gemm1_rows, min_tokens, max_tokens)
|
||||
for key, n in reps.items():
|
||||
if key in _WARMED:
|
||||
continue
|
||||
x = torch.zeros(n, hidden, dtype=torch.bfloat16, device=weights.w1.device)
|
||||
ids = torch.randint(0, num_experts, (n, top_k), device=x.device)
|
||||
tw = torch.full((n, top_k), 1.0 / top_k, device=x.device)
|
||||
grouped_moe(
|
||||
x,
|
||||
ids.int(),
|
||||
tw,
|
||||
weights.w1,
|
||||
weights.w1_sf,
|
||||
weights.w1_alpha,
|
||||
weights.w2,
|
||||
weights.w2_sf,
|
||||
weights.w2_alpha,
|
||||
)
|
||||
_WARMED.add(key)
|
||||
torch.accelerator.synchronize()
|
||||
@@ -0,0 +1,573 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Expert-indexed small-M NVFP4 GEMV kernels (CuTe DSL) for the low-latency
|
||||
decode MoE block, built on the dense GEMV design of ``nvfp4_gemv``: each CTA
|
||||
owns 16 output rows (the m16n8k16 M dimension, swap-AB), its warps split K and
|
||||
reduce through shared memory, weights are dequantized exactly to BF16
|
||||
(``cvt.rn.bf16x2.e2m1x2`` + ``mul.rn.bf16x2``) and the fp32 global scale is
|
||||
applied to the fp32 accumulator.
|
||||
|
||||
``gate_up``: one (token, expert) pair per CTA column of the grid; writes
|
||||
``bf16(silu(g) * u)`` from fp32 gate/up with FlashInfer's approximate sigmoid
|
||||
(the shared expert rounds its gate/up to BF16 first and uses the exact SiLU,
|
||||
as its dense GEMM does). Expert weights use FlashInfer's W4A16
|
||||
layout ([up; gate] in 64-row groups, 6D MMA scales).
|
||||
|
||||
``down_combine``: per token and 16 output rows, the down GEMVs of the token's
|
||||
top-k experts and of the shared expert, all weights prefetched before
|
||||
``griddepcontrol.wait``; ``out = bf16(bf16(sum_j tw_j * y_j) + y_shared)``
|
||||
with ``y = bf16(acc * global_scale)``.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
|
||||
_CHUNK = 64
|
||||
_WARPS = 4
|
||||
_SPLIT = 2
|
||||
|
||||
_classes: Any = None
|
||||
_compiled: dict[tuple, Any] = {}
|
||||
|
||||
|
||||
def _build():
|
||||
import cutlass
|
||||
import cutlass.cute as cute
|
||||
from cuda.bindings.driver import CUstream
|
||||
from cutlass import Float32, Int32, Int64, Uint32
|
||||
from cutlass.cutlass_dsl import dsl_user_op
|
||||
|
||||
from .cute_primitives import (
|
||||
alloc_reduce,
|
||||
cta_reduce,
|
||||
decode,
|
||||
ld_x16,
|
||||
load_rows,
|
||||
mma,
|
||||
ptx,
|
||||
round_bf16,
|
||||
scale_pair,
|
||||
silu_mul,
|
||||
st_out,
|
||||
)
|
||||
|
||||
@dsl_user_op
|
||||
def silu_mul_approx(g: Float32, u: Float32, *, loc=None, ip=None):
|
||||
# g * rcp.approx(1 + ex2.approx(-g * log2(e))) * u, the instructions of
|
||||
# FlashInfer's W4A16 MoE epilogue (routed experts).
|
||||
return ptx(
|
||||
"{\n.reg .f32 e, d;\n"
|
||||
"mul.f32 e, $1, 0fBFB8AA3B;\nex2.approx.ftz.f32 e, e;\n"
|
||||
"add.f32 d, e, 0f3F800000;\nrcp.approx.ftz.f32 d, d;\n"
|
||||
"mul.f32 d, $1, d;\nmul.f32 $0, d, $2;\n}",
|
||||
"=f,f,f",
|
||||
[g, u],
|
||||
Float32,
|
||||
effects=False,
|
||||
loc=loc,
|
||||
ip=ip,
|
||||
)
|
||||
|
||||
@cute.jit
|
||||
def gemv_pass(
|
||||
acc,
|
||||
a_off: cutlass.Constexpr,
|
||||
qs,
|
||||
ss,
|
||||
q_off: cutlass.Constexpr,
|
||||
s_off: cutlass.Constexpr,
|
||||
x_base,
|
||||
CPW: cutlass.Constexpr,
|
||||
valid,
|
||||
):
|
||||
"""acc[a_off:a_off+4] += W_rows (16 x 64*CPW) @ x (token column 0)."""
|
||||
for c in cutlass.range_constexpr(CPW):
|
||||
xv = ld_x16(x_base + c * 128, valid)
|
||||
sg = scale_pair(ss[s_off + c * 2])
|
||||
sg8 = scale_pair(ss[s_off + c * 2 + 1])
|
||||
q0 = q_off + c * 4
|
||||
for j in cutlass.range_constexpr(4):
|
||||
a = decode(qs[q0 + j // 2], qs[q0 + 2 + j // 2], j, sg, sg8)
|
||||
r = mma(
|
||||
[acc[a_off], acc[a_off + 1], acc[a_off + 2], acc[a_off + 3]],
|
||||
a,
|
||||
xv[2 * j],
|
||||
xv[2 * j + 1],
|
||||
)
|
||||
for i in cutlass.range_constexpr(4):
|
||||
acc[a_off + i] = r[i]
|
||||
|
||||
class GateUp:
|
||||
def __init__(self, h, inter, si, topk):
|
||||
self.h, self.inter, self.si, self.topk = h, inter, si, topk
|
||||
assert si % inter == 0
|
||||
self.kc = h // _CHUNK
|
||||
assert self.kc % (_WARPS * _SPLIT) == 0
|
||||
self.cpw = self.kc // (_WARPS * _SPLIT)
|
||||
|
||||
@cute.jit
|
||||
def __call__(
|
||||
self,
|
||||
x: cute.Tensor,
|
||||
ids: cute.Tensor,
|
||||
w: cute.Tensor,
|
||||
s: cute.Tensor,
|
||||
gs: cute.Tensor,
|
||||
act: cute.Tensor,
|
||||
wsh: cute.Tensor,
|
||||
ssh: cute.Tensor,
|
||||
gssh: cute.Tensor,
|
||||
sact: cute.Tensor,
|
||||
P: Int32,
|
||||
NSH: Int32,
|
||||
stream: CUstream,
|
||||
):
|
||||
self.kernel(x, ids, w, s, gs, act, wsh, ssh, gssh, sact, P).launch(
|
||||
grid=[self.inter // 16 * _SPLIT, P + NSH, 1],
|
||||
block=[32 * _WARPS, 1, 1],
|
||||
cluster=[_SPLIT, 1, 1],
|
||||
stream=stream,
|
||||
use_pdl=True,
|
||||
)
|
||||
|
||||
@cute.kernel
|
||||
def kernel(
|
||||
self,
|
||||
x: cute.Tensor,
|
||||
ids: cute.Tensor,
|
||||
w: cute.Tensor,
|
||||
s: cute.Tensor,
|
||||
gs: cute.Tensor,
|
||||
act: cute.Tensor,
|
||||
wsh: cute.Tensor,
|
||||
ssh: cute.Tensor,
|
||||
gssh: cute.Tensor,
|
||||
sact: cute.Tensor,
|
||||
P: Int32,
|
||||
):
|
||||
H: cutlass.Constexpr = self.h
|
||||
INTER: cutlass.Constexpr = self.inter
|
||||
KC: cutlass.Constexpr = self.kc
|
||||
CPW: cutlass.Constexpr = self.cpw
|
||||
WARPS: cutlass.Constexpr = _WARPS
|
||||
tidx, _, _ = cute.arch.thread_idx()
|
||||
SPLIT: cutlass.Constexpr = _SPLIT
|
||||
bx, p, _ = cute.arch.block_idx()
|
||||
nb = bx // SPLIT
|
||||
rank = bx % SPLIT
|
||||
warp = tidx // 32
|
||||
lane = tidx % 32
|
||||
g = lane // 4
|
||||
t = lane % 4
|
||||
c_begin = (rank * WARPS + warp) * CPW
|
||||
SI: cutlass.Constexpr = self.si
|
||||
# Programs p >= P are the shared expert: token (p - P) // halves,
|
||||
# columns ((p - P) % halves) * INTER + 16 * nb.
|
||||
HALVES: cutlass.Constexpr = self.si // self.inter
|
||||
row_g = Int32(0)
|
||||
row_u = Int32(0)
|
||||
w_base = Int64(0)
|
||||
s_base = Int64(0)
|
||||
gsv = Float32(0.0)
|
||||
tok = Int32(0)
|
||||
o_off = Int64(0)
|
||||
qs = cute.make_rmem_tensor((2 * CPW * 4,), Uint32)
|
||||
ss = cute.make_rmem_tensor((2 * CPW * 2,), Uint32)
|
||||
if p < P:
|
||||
cute.arch.griddepcontrol_wait()
|
||||
e = Int64(ids[p])
|
||||
# FlashInfer GEMM1 rows: [up 64; gate 64] per 128-row group.
|
||||
row_u = (nb * 16) // 64 * 128 + (nb * 16) % 64 + g
|
||||
row_g = row_u + 64
|
||||
w_base = w.iterator.toint() + e * (2 * INTER * KC * 32)
|
||||
s_base = s.iterator.toint() + e * ((2 * INTER // 128) * KC * 512)
|
||||
gsv = gs[e]
|
||||
tok = p // self.topk
|
||||
o_off = Int64(p) * INTER + nb * 16
|
||||
load_rows(
|
||||
w_base, s_base, row_g, KC, CPW, c_begin, t, qs, ss, 0, 0, True
|
||||
)
|
||||
load_rows(
|
||||
w_base,
|
||||
s_base,
|
||||
row_u,
|
||||
KC,
|
||||
CPW,
|
||||
c_begin,
|
||||
t,
|
||||
qs,
|
||||
ss,
|
||||
CPW * 4,
|
||||
CPW * 2,
|
||||
True,
|
||||
)
|
||||
else:
|
||||
# Shared expert: static weights, loaded before the wait.
|
||||
v = p - P
|
||||
tok = v // HALVES
|
||||
c0 = (v % HALVES) * INTER + nb * 16
|
||||
row_g = c0 + g
|
||||
row_u = SI + c0 + g
|
||||
w_base = wsh.iterator.toint()
|
||||
s_base = ssh.iterator.toint()
|
||||
gsv = gssh[0]
|
||||
o_off = Int64(tok) * SI + c0
|
||||
load_rows(
|
||||
w_base, s_base, row_g, KC, CPW, c_begin, t, qs, ss, 0, 0, True
|
||||
)
|
||||
load_rows(
|
||||
w_base,
|
||||
s_base,
|
||||
row_u,
|
||||
KC,
|
||||
CPW,
|
||||
c_begin,
|
||||
t,
|
||||
qs,
|
||||
ss,
|
||||
CPW * 4,
|
||||
CPW * 2,
|
||||
True,
|
||||
)
|
||||
cute.arch.griddepcontrol_wait()
|
||||
cute.arch.cluster_arrive_relaxed()
|
||||
x_base = x.iterator.toint() + (Int64(tok) * H + c_begin * 64 + t * 16) * 2
|
||||
valid = Int32(g == 0)
|
||||
acc = cute.make_rmem_tensor((8,), Float32)
|
||||
acc.fill(0.0)
|
||||
gemv_pass(acc, 0, qs, ss, 0, 0, x_base, CPW, valid)
|
||||
gemv_pass(acc, 4, qs, ss, CPW * 4, CPW * 2, x_base, CPW, valid)
|
||||
cute.arch.griddepcontrol_launch_dependents()
|
||||
smem = cutlass.memory.SmemAllocator()
|
||||
red, part = alloc_reduce(smem, 8, WARPS, SPLIT)
|
||||
cta_reduce(acc, 8, WARPS, SPLIT, warp, lane, rank, red, part)
|
||||
if warp == 0 and rank == 0 and t == 0:
|
||||
a_base = act.iterator.toint()
|
||||
if p >= P:
|
||||
a_base = sact.iterator.toint()
|
||||
# Routed experts keep gate/up in fp32 (TRTLLM MoE rule); the
|
||||
# shared expert is a dense MLP whose gate/up GEMM output is BF16.
|
||||
is_shared = p >= P
|
||||
for i in cutlass.range_constexpr(0, 4, 2):
|
||||
gv = acc[i] * gsv
|
||||
uv = acc[4 + i] * gsv
|
||||
gv = cutlass.select_(is_shared, round_bf16(gv), gv)
|
||||
uv = cutlass.select_(is_shared, round_bf16(uv), uv)
|
||||
v = cutlass.select_(
|
||||
is_shared, silu_mul(gv, uv), silu_mul_approx(gv, uv)
|
||||
)
|
||||
off = o_off + g + 8 * (i // 2)
|
||||
st_out(a_base + off * 2, v, Int32(1))
|
||||
|
||||
class DownCombine:
|
||||
def __init__(self, h, inter, si, topk):
|
||||
self.h, self.inter, self.si, self.topk = h, inter, si, topk
|
||||
self.kc = inter // _CHUNK
|
||||
self.kcs = si // _CHUNK
|
||||
parts = _WARPS * _SPLIT
|
||||
assert self.kc % parts == 0 and self.kcs % parts == 0
|
||||
self.cpw = self.kc // parts
|
||||
self.cpws = self.kcs // parts
|
||||
|
||||
@cute.jit
|
||||
def __call__(
|
||||
self,
|
||||
act: cute.Tensor,
|
||||
ids: cute.Tensor,
|
||||
tw: cute.Tensor,
|
||||
w: cute.Tensor,
|
||||
s: cute.Tensor,
|
||||
gs: cute.Tensor,
|
||||
sact: cute.Tensor,
|
||||
wsd: cute.Tensor,
|
||||
ssd: cute.Tensor,
|
||||
gssd: cute.Tensor,
|
||||
out: cute.Tensor,
|
||||
M: Int32,
|
||||
stream: CUstream,
|
||||
):
|
||||
self.kernel(act, ids, tw, w, s, gs, sact, wsd, ssd, gssd, out).launch(
|
||||
grid=[self.h // 16 * _SPLIT, M, 1],
|
||||
block=[32 * _WARPS, 1, 1],
|
||||
cluster=[_SPLIT, 1, 1],
|
||||
stream=stream,
|
||||
use_pdl=True,
|
||||
)
|
||||
|
||||
@cute.kernel
|
||||
def kernel(
|
||||
self,
|
||||
act: cute.Tensor,
|
||||
ids: cute.Tensor,
|
||||
tw: cute.Tensor,
|
||||
w: cute.Tensor,
|
||||
s: cute.Tensor,
|
||||
gs: cute.Tensor,
|
||||
sact: cute.Tensor,
|
||||
wsd: cute.Tensor,
|
||||
ssd: cute.Tensor,
|
||||
gssd: cute.Tensor,
|
||||
out: cute.Tensor,
|
||||
):
|
||||
H: cutlass.Constexpr = self.h
|
||||
INTER: cutlass.Constexpr = self.inter
|
||||
SI: cutlass.Constexpr = self.si
|
||||
TOPK: cutlass.Constexpr = self.topk
|
||||
KC: cutlass.Constexpr = self.kc
|
||||
KCS: cutlass.Constexpr = self.kcs
|
||||
CPW: cutlass.Constexpr = self.cpw
|
||||
CPWS: cutlass.Constexpr = self.cpws
|
||||
WARPS: cutlass.Constexpr = _WARPS
|
||||
tidx, _, _ = cute.arch.thread_idx()
|
||||
SPLIT: cutlass.Constexpr = _SPLIT
|
||||
bx, tok, _ = cute.arch.block_idx()
|
||||
nb = bx // SPLIT
|
||||
rank = bx % SPLIT
|
||||
warp = tidx // 32
|
||||
lane = tidx % 32
|
||||
g = lane // 4
|
||||
t = lane % 4
|
||||
part_id = rank * WARPS + warp
|
||||
row = nb * 16 + g
|
||||
qs = cute.make_rmem_tensor(((TOPK * CPW + CPWS) * 4,), Uint32)
|
||||
ss = cute.make_rmem_tensor(((TOPK * CPW + CPWS) * 2,), Uint32)
|
||||
# Expert ids / weights come from the router stage, which completed
|
||||
# before the previous kernel passed its dependency wait.
|
||||
gsv = cute.make_rmem_tensor((TOPK,), Float32)
|
||||
twv = cute.make_rmem_tensor((TOPK,), Float32)
|
||||
for j in cutlass.range_constexpr(TOPK):
|
||||
e = Int64(ids[tok * TOPK + j])
|
||||
load_rows(
|
||||
w.iterator.toint() + e * (H * KC * 32),
|
||||
s.iterator.toint() + e * ((H // 128) * KC * 512),
|
||||
row,
|
||||
KC,
|
||||
CPW,
|
||||
part_id * CPW,
|
||||
t,
|
||||
qs,
|
||||
ss,
|
||||
j * CPW * 4,
|
||||
j * CPW * 2,
|
||||
True,
|
||||
)
|
||||
gsv[j] = gs[e]
|
||||
twv[j] = tw[tok * TOPK + j]
|
||||
load_rows(
|
||||
wsd.iterator.toint(),
|
||||
ssd.iterator.toint(),
|
||||
row,
|
||||
KCS,
|
||||
CPWS,
|
||||
part_id * CPWS,
|
||||
t,
|
||||
qs,
|
||||
ss,
|
||||
TOPK * CPW * 4,
|
||||
TOPK * CPW * 2,
|
||||
True,
|
||||
)
|
||||
gss = gssd[0]
|
||||
cute.arch.cluster_arrive_relaxed()
|
||||
cute.arch.griddepcontrol_wait()
|
||||
valid = Int32(g == 0)
|
||||
NA: cutlass.Constexpr = (TOPK + 1) * 4
|
||||
acc = cute.make_rmem_tensor((NA,), Float32)
|
||||
acc.fill(0.0)
|
||||
for j in cutlass.range_constexpr(TOPK):
|
||||
x_base = (
|
||||
act.iterator.toint()
|
||||
+ (Int64(tok * TOPK + j) * INTER + part_id * CPW * 64 + t * 16) * 2
|
||||
)
|
||||
gemv_pass(
|
||||
acc, j * 4, qs, ss, j * CPW * 4, j * CPW * 2, x_base, CPW, valid
|
||||
)
|
||||
x_base = (
|
||||
sact.iterator.toint()
|
||||
+ (Int64(tok) * SI + part_id * CPWS * 64 + t * 16) * 2
|
||||
)
|
||||
gemv_pass(
|
||||
acc,
|
||||
TOPK * 4,
|
||||
qs,
|
||||
ss,
|
||||
TOPK * CPW * 4,
|
||||
TOPK * CPW * 2,
|
||||
x_base,
|
||||
CPWS,
|
||||
valid,
|
||||
)
|
||||
cute.arch.griddepcontrol_launch_dependents()
|
||||
smem = cutlass.memory.SmemAllocator()
|
||||
red, part = alloc_reduce(smem, NA, WARPS, SPLIT)
|
||||
cta_reduce(acc, NA, WARPS, SPLIT, warp, lane, rank, red, part)
|
||||
if warp == 0 and rank == 0 and t == 0:
|
||||
o_base = out.iterator.toint()
|
||||
for i in cutlass.range_constexpr(0, 4, 2):
|
||||
routed = Float32(0.0)
|
||||
for j in cutlass.range_constexpr(TOPK):
|
||||
routed = routed + twv[j] * round_bf16(acc[j * 4 + i] * gsv[j])
|
||||
shared = round_bf16(acc[TOPK * 4 + i] * gss)
|
||||
v = round_bf16(routed) + shared
|
||||
r = nb * 16 + g + 8 * (i // 2)
|
||||
st_out(o_base + (Int64(tok) * H + r) * 2, v, Int32(1))
|
||||
|
||||
return GateUp, DownCombine
|
||||
|
||||
|
||||
def _fakes(*specs) -> list:
|
||||
"""Compile-time tensors: (dtype, divisibility) is 1-D of symbolic size,
|
||||
(dtype, divisibility, shape) has that shape."""
|
||||
import cutlass
|
||||
import cutlass.cute as cute
|
||||
from quack.compile_utils import make_fake_tensor
|
||||
|
||||
return [
|
||||
make_fake_tensor(
|
||||
getattr(cutlass, spec[0]),
|
||||
spec[2] if len(spec) > 2 else (cute.sym_int(),),
|
||||
divisibility=spec[1],
|
||||
)
|
||||
for spec in specs
|
||||
]
|
||||
|
||||
|
||||
def _flat_bytes(t: torch.Tensor) -> torch.Tensor:
|
||||
"""The contiguous storage behind a (possibly permuted) scale view."""
|
||||
t = t.view(torch.uint8)
|
||||
return torch.as_strided(t, (t.numel(),), (1,), t.storage_offset())
|
||||
|
||||
|
||||
def _stream():
|
||||
from cuda.bindings.driver import CUstream
|
||||
|
||||
return CUstream(torch.cuda.current_stream().cuda_stream)
|
||||
|
||||
|
||||
def _compile(key: tuple, fakes: list, *scalars):
|
||||
"""Compile kernel ``key = (kind, h, inter, si, topk)`` into ``_compiled``."""
|
||||
global _classes
|
||||
import cutlass.cute as cute
|
||||
|
||||
if _classes is None:
|
||||
_classes = _build()
|
||||
kern = _classes[key[0] == "down"](*key[1:])
|
||||
fn = _compiled[key] = cute.compile(
|
||||
kern, *fakes, *scalars, _stream(), options="--enable-tvm-ffi"
|
||||
)
|
||||
return fn
|
||||
|
||||
|
||||
def gate_up(
|
||||
x: torch.Tensor,
|
||||
ids: torch.Tensor,
|
||||
w1: torch.Tensor,
|
||||
w1_sf: torch.Tensor,
|
||||
alpha: torch.Tensor,
|
||||
act: torch.Tensor,
|
||||
wsh: torch.Tensor,
|
||||
ssh: torch.Tensor,
|
||||
gssh: torch.Tensor,
|
||||
sact: torch.Tensor,
|
||||
) -> None:
|
||||
"""act[p] = silu_and_mul((x[p // topk] @ W1[ids[p]]^T) * alpha) on fp32
|
||||
gate/up for the FlashInfer-layout NVFP4 experts (w1 uint8 [E, 2I, H/2]),
|
||||
and in the same launch sact[t] = silu_and_mul(bf16((x[t] @ Wsh^T) * gssh))
|
||||
for the shared expert (ModelOpt [gate; up] uint8 [2 SI, H/2], flat
|
||||
128x4-swizzled scales)."""
|
||||
m, h = x.shape
|
||||
p, topk = ids.numel(), ids.shape[1]
|
||||
inter = w1.shape[1] // 2
|
||||
si = wsh.shape[0] // 2
|
||||
key = ("gu", h, inter, si, topk)
|
||||
fn = _compiled.get(key)
|
||||
if fn is None:
|
||||
fn = _compile(
|
||||
key,
|
||||
_fakes(
|
||||
("BFloat16", 8),
|
||||
("Int32", 1),
|
||||
("Uint8", 16),
|
||||
("Uint8", 4),
|
||||
("Float32", 1),
|
||||
("BFloat16", 1),
|
||||
("Uint8", 16),
|
||||
("Uint8", 4),
|
||||
("Float32", 1, (1,)),
|
||||
("BFloat16", 1),
|
||||
),
|
||||
1,
|
||||
1,
|
||||
)
|
||||
fn(
|
||||
x.view(-1),
|
||||
ids.view(-1),
|
||||
w1.view(-1),
|
||||
_flat_bytes(w1_sf),
|
||||
alpha,
|
||||
act.view(-1),
|
||||
wsh.view(torch.uint8).view(-1),
|
||||
ssh.view(-1),
|
||||
gssh.reshape(1),
|
||||
sact.view(-1),
|
||||
p,
|
||||
m * (si // inter),
|
||||
_stream(),
|
||||
)
|
||||
|
||||
|
||||
def down_combine(
|
||||
act: torch.Tensor,
|
||||
ids: torch.Tensor,
|
||||
tw: torch.Tensor,
|
||||
w2: torch.Tensor,
|
||||
w2_sf: torch.Tensor,
|
||||
alpha2: torch.Tensor,
|
||||
sact: torch.Tensor,
|
||||
wsd: torch.Tensor,
|
||||
ssd: torch.Tensor,
|
||||
gssd: torch.Tensor,
|
||||
out: torch.Tensor,
|
||||
) -> None:
|
||||
"""out[t] = bf16(bf16(sum_j tw * bf16(act_j @ W2[e_j]^T * alpha2)) +
|
||||
bf16(sact[t] @ Wsd^T * gssd))."""
|
||||
m, topk = ids.shape
|
||||
h = w2.shape[1]
|
||||
inter = w2.shape[2] * 2
|
||||
si = wsd.shape[1] * 2
|
||||
key = ("down", h, inter, si, topk)
|
||||
fn = _compiled.get(key)
|
||||
if fn is None:
|
||||
fn = _compile(
|
||||
key,
|
||||
_fakes(
|
||||
("BFloat16", 8),
|
||||
("Int32", 1),
|
||||
("Float32", 1),
|
||||
("Uint8", 16),
|
||||
("Uint8", 4),
|
||||
("Float32", 1),
|
||||
("BFloat16", 8),
|
||||
("Uint8", 16),
|
||||
("Uint8", 4),
|
||||
("Float32", 1, (1,)),
|
||||
("BFloat16", 1),
|
||||
),
|
||||
1,
|
||||
)
|
||||
fn(
|
||||
act.view(-1),
|
||||
ids.view(-1),
|
||||
tw.view(-1),
|
||||
w2.view(-1),
|
||||
_flat_bytes(w2_sf),
|
||||
alpha2,
|
||||
sact.view(-1),
|
||||
wsd.view(-1),
|
||||
ssd.view(-1),
|
||||
gssd.reshape(1),
|
||||
out.view(-1),
|
||||
m,
|
||||
_stream(),
|
||||
)
|
||||
@@ -0,0 +1,130 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Flint's RMSNorm, fused residual-add RMSNorm and SiLU-and-mul (Triton).
|
||||
|
||||
Numerics of the reference implementation's CUDA norm and activation kernels,
|
||||
everything in fp32 with one output rounding:
|
||||
|
||||
- ``rms_norm``: ``bf16(x * rsqrt(mean(x^2) + eps) * w)``.
|
||||
- ``fused_add_rms_norm``: ``s = x + residual`` in fp32,
|
||||
``residual_out = bf16(s)``, ``out = bf16(s * rsqrt(mean(s^2) + eps) * w)``
|
||||
from the unrounded ``s``.
|
||||
- ``silu_and_mul``: ``bf16(g / (1 + exp(-g)) * u)`` (precise ``expf`` and
|
||||
division).
|
||||
|
||||
``rsqrt`` is ``rsqrt.approx.ftz.f32`` (``rsqrtf`` under fast math). The fused
|
||||
Flint kernels (``qkv_fused``, the decode MoE block's norm and shared expert, and
|
||||
the GEMV SiLU epilogue) follow the same rules; the routed experts use the
|
||||
approximate ``ex2`` / ``rcp`` SiLU of the TRTLLM MoE (see ``moe``).
|
||||
"""
|
||||
|
||||
import torch
|
||||
from torch import nn
|
||||
|
||||
from vllm.triton_utils import tl, tldevice, triton
|
||||
|
||||
|
||||
@triton.jit
|
||||
def _rsqrt(x):
|
||||
return tl.inline_asm_elementwise(
|
||||
"rsqrt.approx.ftz.f32 $0, $1;", "=f,f", [x], tl.float32, True, 1
|
||||
)
|
||||
|
||||
|
||||
@triton.jit
|
||||
def _rms_norm_kernel(
|
||||
x_ptr,
|
||||
res_ptr,
|
||||
w_ptr,
|
||||
out_ptr,
|
||||
res_out_ptr,
|
||||
x_stride,
|
||||
eps,
|
||||
H: tl.constexpr,
|
||||
HAS_RESIDUAL: tl.constexpr,
|
||||
):
|
||||
row = tl.program_id(0).to(tl.int64)
|
||||
offs = tl.arange(0, H)
|
||||
w = tl.load(w_ptr + offs).to(tl.float32)
|
||||
s = tl.load(x_ptr + row * x_stride + offs).to(tl.float32)
|
||||
if HAS_RESIDUAL:
|
||||
s += tl.load(res_ptr + row * H + offs).to(tl.float32)
|
||||
tl.store(res_out_ptr + row * H + offs, s.to(tl.bfloat16))
|
||||
rs = _rsqrt(tl.sum(s * s, axis=0) / H + eps)
|
||||
tl.store(out_ptr + row * H + offs, (s * rs * w).to(tl.bfloat16))
|
||||
|
||||
|
||||
def _launch_norm(x, residual, weight, eps, out):
|
||||
m, h = x.shape
|
||||
assert x.dtype == torch.bfloat16 and x.stride(-1) == 1
|
||||
assert weight.shape == (h,) and h & (h - 1) == 0
|
||||
if m > 0:
|
||||
_rms_norm_kernel[(m,)](
|
||||
x,
|
||||
x if residual is None else residual,
|
||||
weight,
|
||||
out,
|
||||
residual,
|
||||
x.stride(0),
|
||||
eps,
|
||||
H=h,
|
||||
HAS_RESIDUAL=residual is not None,
|
||||
num_warps=4,
|
||||
)
|
||||
|
||||
|
||||
def rms_norm(x: torch.Tensor, weight: torch.Tensor, eps: float) -> torch.Tensor:
|
||||
out = torch.empty(x.shape, dtype=x.dtype, device=x.device)
|
||||
_launch_norm(x, None, weight, eps, out)
|
||||
return out
|
||||
|
||||
|
||||
def fused_add_rms_norm(
|
||||
x: torch.Tensor, residual: torch.Tensor, weight: torch.Tensor, eps: float
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
"""In place: ``residual = bf16(x + residual)``, ``x = rms_norm(x +
|
||||
residual)``; returns ``(x, residual)``."""
|
||||
assert residual.shape == x.shape and residual.is_contiguous()
|
||||
assert x.is_contiguous()
|
||||
_launch_norm(x, residual, weight, eps, x)
|
||||
return x, residual
|
||||
|
||||
|
||||
@triton.jit
|
||||
def _silu_and_mul_kernel(x_ptr, out_ptr, x_stride, D: tl.constexpr, BD: tl.constexpr):
|
||||
row = tl.program_id(0).to(tl.int64)
|
||||
offs = tl.program_id(1) * BD + tl.arange(0, BD)
|
||||
g = tl.load(x_ptr + row * x_stride + offs).to(tl.float32)
|
||||
u = tl.load(x_ptr + row * x_stride + D + offs).to(tl.float32)
|
||||
silu = tl.math.div_rn(g, 1.0 + tldevice.exp(-g))
|
||||
tl.store(out_ptr + row * D + offs, (silu * u).to(tl.bfloat16))
|
||||
|
||||
|
||||
def silu_and_mul(x: torch.Tensor) -> torch.Tensor:
|
||||
"""``silu(x[:, :D]) * x[:, D:]`` for ``x`` [M, 2D]."""
|
||||
m, d2 = x.shape
|
||||
d = d2 // 2
|
||||
assert x.dtype == torch.bfloat16 and x.stride(-1) == 1
|
||||
bd = min(d, 1024)
|
||||
assert d % bd == 0
|
||||
out = torch.empty(m, d, dtype=x.dtype, device=x.device)
|
||||
if m > 0:
|
||||
_silu_and_mul_kernel[(m, d // bd)](x, out, x.stride(0), D=d, BD=bd, num_warps=4)
|
||||
return out
|
||||
|
||||
|
||||
class RMSNorm(nn.Module):
|
||||
"""Holds a norm ``weight``; ``rms_norm(x)``, or with a residual
|
||||
``fused_add_rms_norm(x, residual)``."""
|
||||
|
||||
def __init__(self, hidden_size: int, eps: float) -> None:
|
||||
super().__init__()
|
||||
self.weight = nn.Parameter(torch.ones(hidden_size))
|
||||
self.variance_epsilon = eps
|
||||
|
||||
def forward(
|
||||
self, x: torch.Tensor, residual: torch.Tensor | None = None
|
||||
) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
|
||||
if residual is None:
|
||||
return rms_norm(x, self.weight, self.variance_epsilon)
|
||||
return fused_add_rms_norm(x, residual, self.weight, self.variance_epsilon)
|
||||
@@ -0,0 +1,492 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Small-M BF16 x NVFP4 (W4A16) and BF16 x BF16 GEMM for SM100 in CuTe DSL.
|
||||
|
||||
``out[M, N] = (x[M, K] @ dequant(W)[N, K]^T) * global_scale`` with the weight in
|
||||
the ModelOpt layout used by the FlashInfer CuTe-DSL W4A16 kernel: ``weight``
|
||||
uint8 ``[N, K / 2]`` (two E2M1 codes per byte, low nibble first) and
|
||||
``weight_scale`` the 128x4-swizzled E4M3 block scales (one per 16 values), so
|
||||
both kernels share the same tensors.
|
||||
|
||||
Numerics: every weight is dequantized exactly to BF16 (``e2m1 * e4m3`` via
|
||||
``cvt.rn.bf16x2.e2m1x2`` and ``mul.rn.bf16x2``), multiplied with the BF16
|
||||
activations on tensor cores (``mma.sync`` m16n8k16, FP32 accumulate), and the
|
||||
FP32 global scale is applied to the FP32 accumulator before the single output
|
||||
rounding to BF16.
|
||||
|
||||
Each CTA owns 16 output features (the MMA M dimension, swap-AB) and ``8 * nt``
|
||||
tokens (``nt`` = 1, 2 or 4, i.e. up to 32); its warps (and, with ``split > 1``,
|
||||
the CTAs of a cluster) split K and reduce through shared memory / DSMEM. All
|
||||
weight loads are issued before ``griddepcontrol.wait`` so they overlap the
|
||||
previous kernel under PDL.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
|
||||
_CHUNK = 64 # K values per warp step (four m16n8k16 k-tiles)
|
||||
_NACC = 8 # accumulator sets per lane, rotated over k-tiles
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GemvConfig:
|
||||
warps: int
|
||||
split: int
|
||||
|
||||
|
||||
_C = GemvConfig
|
||||
_Table = dict[tuple[int, int], dict[int, GemvConfig]]
|
||||
# Tuned on GB200 (CUDA-graph replay, weights rotated past L2): config per
|
||||
# number of 8-token MMA tiles per CTA (``nt``). A shape / ``nt`` is listed only
|
||||
# where the GEMV beats the alternative (FlashInfer CuTe-DSL for NVFP4, cuBLAS
|
||||
# for BF16) at ``M <= 8 * nt``; the model uses the GEMV only for listed ones.
|
||||
# Gate/up projections always take the SiLU-and-mul tables.
|
||||
_TUNED: _Table = {
|
||||
(5120, 2048): {1: _C(8, 1), 2: _C(8, 2)},
|
||||
(2048, 4096): {1: _C(4, 4), 2: _C(8, 2), 4: _C(4, 4)},
|
||||
(2048, 6144): {1: _C(8, 4), 2: _C(8, 2), 4: _C(4, 8)},
|
||||
(2048, 1024): {1: _C(4, 1), 2: _C(4, 1), 4: _C(8, 1)},
|
||||
}
|
||||
_TUNED_SILU_MUL: _Table = {
|
||||
(12288, 2048): {1: _C(4, 1), 2: _C(2, 2)},
|
||||
(2048, 2048): {1: _C(2, 4), 2: _C(4, 2), 4: _C(8, 2)},
|
||||
}
|
||||
# The BF16 qkv and dense down GEMVs are no faster than cuBLAS.
|
||||
_TUNED_BF16: _Table = {
|
||||
(2048, 4096): {1: _C(4, 8), 2: _C(8, 4)},
|
||||
(2048, 1024): {1: _C(4, 4), 2: _C(4, 4)},
|
||||
}
|
||||
_TUNED_BF16_SILU_MUL: _Table = {
|
||||
(12288, 2048): {1: _C(2, 1)},
|
||||
(2048, 2048): {1: _C(8, 4), 2: _C(4, 4), 4: _C(8, 2)},
|
||||
}
|
||||
|
||||
|
||||
def _table(silu_mul: bool, bf16: bool) -> _Table:
|
||||
if bf16:
|
||||
return _TUNED_BF16_SILU_MUL if silu_mul else _TUNED_BF16
|
||||
return _TUNED_SILU_MUL if silu_mul else _TUNED
|
||||
|
||||
|
||||
def _configs(n: int, k: int, silu_mul: bool, bf16: bool) -> dict[int, GemvConfig]:
|
||||
return _table(silu_mul, bf16)[(n, k)]
|
||||
|
||||
|
||||
def tuned(n: int, k: int, silu_mul: bool = False, bf16: bool = False) -> bool:
|
||||
"""Whether the GEMV is the fastest choice for this shape at some M."""
|
||||
return (n, k) in _table(silu_mul, bf16)
|
||||
|
||||
|
||||
def pick_nt(
|
||||
n: int, k: int, m: int, silu_mul: bool = False, bf16: bool = False
|
||||
) -> int | None:
|
||||
"""Smallest tuned token tile count covering ``m``, or None if the GEMV
|
||||
is not the fastest choice for this shape and M."""
|
||||
nts = [nt for nt in _table(silu_mul, bf16).get((n, k), ()) if 8 * nt >= m]
|
||||
return min(nts) if nts else None
|
||||
|
||||
|
||||
def _build_kernel_class():
|
||||
import cutlass
|
||||
import cutlass.cute as cute
|
||||
from cuda.bindings.driver import CUstream
|
||||
from cutlass import Float32, Int32, Int64, Uint32, const_expr
|
||||
|
||||
from .cute_primitives import (
|
||||
alloc_reduce,
|
||||
cta_reduce,
|
||||
decode,
|
||||
ld_w16,
|
||||
ld_x16,
|
||||
load_rows,
|
||||
mma,
|
||||
round_bf16,
|
||||
scale_pair,
|
||||
silu_mul,
|
||||
st_out,
|
||||
)
|
||||
|
||||
class Nvfp4Gemv:
|
||||
def __init__(self, k, nt, warps, split, pdl, silu_mul, bf16):
|
||||
self.bf16 = bf16
|
||||
self.kc = k // _CHUNK
|
||||
assert self.kc % (warps * split) == 0
|
||||
self.cpw = self.kc // (warps * split)
|
||||
self.nt = nt
|
||||
self.warps = warps
|
||||
self.split = split
|
||||
self.pdl = pdl
|
||||
# silu_mul: each CTA also owns the matching "up" row block N/2
|
||||
# rows below and writes silu(gate) * up.
|
||||
self.rb = 2 if silu_mul else 1
|
||||
self.nacc = _NACC // self.rb
|
||||
|
||||
@cute.jit
|
||||
def __call__(
|
||||
self,
|
||||
x: cute.Tensor,
|
||||
w: cute.Tensor,
|
||||
s: cute.Tensor,
|
||||
gs: cute.Tensor,
|
||||
out: cute.Tensor,
|
||||
M: Int32,
|
||||
N: Int32,
|
||||
stride_x: Int32,
|
||||
stride_o: Int32,
|
||||
stream: CUstream,
|
||||
):
|
||||
tokens = 8 * self.nt
|
||||
n_out = N // self.rb
|
||||
self.kernel(x, w, s, gs, out, M, n_out, stride_x, stride_o).launch(
|
||||
grid=[(n_out // 16) * self.split, (M + tokens - 1) // tokens, 1],
|
||||
block=[32 * self.warps, 1, 1],
|
||||
cluster=[self.split, 1, 1],
|
||||
stream=stream,
|
||||
use_pdl=self.pdl,
|
||||
)
|
||||
|
||||
@cute.kernel
|
||||
def kernel(
|
||||
self,
|
||||
x: cute.Tensor,
|
||||
w: cute.Tensor,
|
||||
s: cute.Tensor,
|
||||
gs: cute.Tensor,
|
||||
out: cute.Tensor,
|
||||
M: Int32,
|
||||
n_out: Int32,
|
||||
stride_x: Int32,
|
||||
stride_o: Int32,
|
||||
):
|
||||
NT: cutlass.Constexpr = self.nt
|
||||
CPW: cutlass.Constexpr = self.cpw
|
||||
KC: cutlass.Constexpr = self.kc
|
||||
WARPS: cutlass.Constexpr = self.warps
|
||||
SPLIT: cutlass.Constexpr = self.split
|
||||
NACC: cutlass.Constexpr = self.nacc
|
||||
RB: cutlass.Constexpr = self.rb
|
||||
NA: cutlass.Constexpr = RB * NT * 4 # accumulators per lane
|
||||
BF16W: cutlass.Constexpr = self.bf16
|
||||
KB: cutlass.Constexpr = self.kc * (128 if self.bf16 else 32)
|
||||
QW: cutlass.Constexpr = 16 if self.bf16 else 4 # words per chunk
|
||||
tidx, _, _ = cute.arch.thread_idx()
|
||||
bx, mt, _ = cute.arch.block_idx()
|
||||
nb = bx // SPLIT
|
||||
rank = bx % SPLIT
|
||||
warp = tidx // 32
|
||||
lane = tidx % 32
|
||||
g = lane // 4
|
||||
t = lane % 4
|
||||
c_begin = (rank * WARPS + warp) * CPW
|
||||
|
||||
# Lane (g, t) of k-tile j covers k = 64c + 16t + 4j + {0..3} of
|
||||
# rows g and g + 8, i.e. bytes 8t + 2j + {0, 1} of the 32-byte
|
||||
# chunk row, and scale group 4c + t of both rows.
|
||||
qs = cute.make_rmem_tensor((RB * CPW * QW,), Uint32)
|
||||
ss = cute.make_rmem_tensor((RB * CPW * 2,), Uint32)
|
||||
for rb in cutlass.range_constexpr(RB):
|
||||
row_g = nb * 16 + g + rb * n_out
|
||||
if const_expr(BF16W):
|
||||
# 16 BF16 weights of rows g and g + 8 per chunk.
|
||||
wbase = (
|
||||
w.iterator.toint() + Int64(row_g) * KB + c_begin * 128 + t * 32
|
||||
)
|
||||
for c in cutlass.range_constexpr(CPW):
|
||||
q0 = (rb * CPW + c) * QW
|
||||
for h in cutlass.range_constexpr(4):
|
||||
off = (h // 2) * 8 * KB + c * 128 + (h % 2) * 16
|
||||
wv = ld_w16(wbase + off)
|
||||
for i in cutlass.range_constexpr(4):
|
||||
qs[q0 + h * 4 + i] = wv[i]
|
||||
else:
|
||||
load_rows(
|
||||
w.iterator.toint(),
|
||||
s.iterator.toint(),
|
||||
row_g,
|
||||
KC,
|
||||
CPW,
|
||||
c_begin,
|
||||
t,
|
||||
qs,
|
||||
ss,
|
||||
rb * CPW * QW,
|
||||
rb * CPW * 2,
|
||||
)
|
||||
gsv = Float32(1.0)
|
||||
if const_expr(not BF16W):
|
||||
gsv = gs[0]
|
||||
if const_expr(SPLIT > 1):
|
||||
# Peers must be running before DSMEM stores; waited on below.
|
||||
cute.arch.cluster_arrive_relaxed()
|
||||
|
||||
if const_expr(self.pdl):
|
||||
cute.arch.griddepcontrol_wait()
|
||||
xbase = x.iterator.toint() + (Int64(c_begin) * _CHUNK + t * 16) * 2
|
||||
xall = []
|
||||
for c in cutlass.range_constexpr(CPW):
|
||||
xs = []
|
||||
for nt in cutlass.range_constexpr(NT):
|
||||
tok = mt * (8 * NT) + nt * 8 + g
|
||||
addr = xbase + Int64(tok) * stride_x * 2 + c * 128
|
||||
xs.append(ld_x16(addr, Int32(tok < M)))
|
||||
xall.append(xs)
|
||||
|
||||
acc = cute.make_rmem_tensor((NACC * NA,), Float32)
|
||||
acc.fill(0.0)
|
||||
for rb in cutlass.range_constexpr(RB):
|
||||
for c in cutlass.range_constexpr(CPW):
|
||||
q0 = (rb * CPW + c) * QW
|
||||
if const_expr(not BF16W):
|
||||
sg = scale_pair(ss[(rb * CPW + c) * 2])
|
||||
sg8 = scale_pair(ss[(rb * CPW + c) * 2 + 1])
|
||||
for j in cutlass.range_constexpr(4):
|
||||
if const_expr(BF16W):
|
||||
# rows g / g + 8, k pairs (4j, 4j+1) and (4j+2, 4j+3)
|
||||
a = [
|
||||
qs[q0 + 2 * j],
|
||||
qs[q0 + 8 + 2 * j],
|
||||
qs[q0 + 2 * j + 1],
|
||||
qs[q0 + 8 + 2 * j + 1],
|
||||
]
|
||||
else:
|
||||
a = decode(qs[q0 + j // 2], qs[q0 + 2 + j // 2], j, sg, sg8)
|
||||
for nt in cutlass.range_constexpr(NT):
|
||||
xv = xall[c][nt]
|
||||
ab = ((c * 4 + j) % NACC) * NA + (rb * NT + nt) * 4
|
||||
r = mma(
|
||||
[acc[ab], acc[ab + 1], acc[ab + 2], acc[ab + 3]],
|
||||
a,
|
||||
xv[2 * j],
|
||||
xv[2 * j + 1],
|
||||
)
|
||||
for i in cutlass.range_constexpr(4):
|
||||
acc[ab + i] = r[i]
|
||||
for h in cutlass.range_constexpr(1, NACC):
|
||||
for i in cutlass.range_constexpr(NA):
|
||||
acc[i] = acc[i] + acc[h * NA + i]
|
||||
if const_expr(self.pdl):
|
||||
cute.arch.griddepcontrol_launch_dependents()
|
||||
|
||||
smem = cutlass.memory.SmemAllocator()
|
||||
red, part = alloc_reduce(smem, NA, WARPS, SPLIT)
|
||||
cta_reduce(acc, NA, WARPS, SPLIT, warp, lane, rank, red, part)
|
||||
if warp == 0 and rank == 0:
|
||||
obase = out.iterator.toint()
|
||||
for nt in cutlass.range_constexpr(NT):
|
||||
for i in cutlass.range_constexpr(4):
|
||||
v = acc[nt * 4 + i] * gsv
|
||||
if const_expr(RB == 2):
|
||||
u = acc[(NT + nt) * 4 + i] * gsv
|
||||
v = silu_mul(round_bf16(v), round_bf16(u))
|
||||
row = nb * 16 + g + 8 * (i // 2)
|
||||
tok = mt * (8 * NT) + nt * 8 + 2 * t + (i % 2)
|
||||
st_out(
|
||||
obase + (Int64(tok) * stride_o + row) * 2,
|
||||
v,
|
||||
Int32(tok < M),
|
||||
)
|
||||
|
||||
return Nvfp4Gemv
|
||||
|
||||
|
||||
_kernel_cls: Any = None
|
||||
_compiled: dict[tuple, Any] = {}
|
||||
|
||||
|
||||
def _compile(key: tuple):
|
||||
global _kernel_cls
|
||||
import cutlass
|
||||
import cutlass.cute as cute
|
||||
from cuda.bindings.driver import CUstream
|
||||
from quack.compile_utils import make_fake_tensor
|
||||
|
||||
k, nt, cfg, pdl, silu_mul, bf16 = key
|
||||
if _kernel_cls is None:
|
||||
_kernel_cls = _build_kernel_class()
|
||||
sym = cute.sym_int
|
||||
x = make_fake_tensor(cutlass.BFloat16, (sym(), k), divisibility=8)
|
||||
w = make_fake_tensor(cutlass.Uint8, (sym(),), divisibility=16)
|
||||
s = make_fake_tensor(cutlass.Uint8, (sym(),), divisibility=4)
|
||||
gs = make_fake_tensor(cutlass.Float32, (1,), divisibility=1)
|
||||
out = make_fake_tensor(cutlass.BFloat16, (sym(), sym()), divisibility=1)
|
||||
kernel = _kernel_cls(k, nt, cfg.warps, cfg.split, pdl, silu_mul, bf16)
|
||||
stream = CUstream(torch.cuda.current_stream().cuda_stream)
|
||||
return cute.compile(
|
||||
kernel, x, w, s, gs, out, 1, 128, k, 128, stream, options="--enable-tvm-ffi"
|
||||
)
|
||||
|
||||
|
||||
def _get_compiled(n: int, k: int, nt: int, silu_mul: bool, pdl: bool, bf16: bool):
|
||||
cfg = _configs(n, k, silu_mul, bf16)[nt]
|
||||
key = (k, nt, cfg, pdl, silu_mul, bf16)
|
||||
fn = _compiled.get(key)
|
||||
if fn is None:
|
||||
fn = _compiled[key] = _compile(key)
|
||||
return fn
|
||||
|
||||
|
||||
def _launch(x, w, s, gs, n, silu_mul, pdl, bf16) -> torch.Tensor:
|
||||
from cuda.bindings.driver import CUstream
|
||||
|
||||
m, k = x.shape
|
||||
assert m > 0 and x.stride(1) == 1 and x.dtype == torch.bfloat16
|
||||
nt = pick_nt(n, k, m, silu_mul, bf16)
|
||||
assert nt is not None, (n, k, m, silu_mul, bf16)
|
||||
out = torch.empty(m, n // 2 if silu_mul else n, dtype=x.dtype, device=x.device)
|
||||
fn = _get_compiled(n, k, nt, silu_mul, pdl, bf16)
|
||||
fn(
|
||||
x,
|
||||
w,
|
||||
s,
|
||||
gs,
|
||||
out,
|
||||
m,
|
||||
n,
|
||||
x.stride(0),
|
||||
out.stride(0),
|
||||
CUstream(torch.cuda.current_stream().cuda_stream),
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def nvfp4_gemv(
|
||||
x: torch.Tensor,
|
||||
weight: torch.Tensor,
|
||||
weight_scale: torch.Tensor,
|
||||
global_scale: torch.Tensor,
|
||||
silu_mul: bool = False,
|
||||
pdl: bool = True,
|
||||
) -> torch.Tensor:
|
||||
"""Small-M W4A16 GEMM: ``x @ dequant(weight).T * global_scale``.
|
||||
|
||||
Args:
|
||||
x: BF16 ``[M, K]`` activations with unit inner stride; ``(N, K)``
|
||||
and M must be covered by the tuned tables (``pick_nt``).
|
||||
weight: uint8 ``[N, K / 2]`` packed E2M1 codes (ModelOpt layout).
|
||||
weight_scale: 128x4-swizzled E4M3 block scales (``swizzle_blockscale``).
|
||||
global_scale: FP32 scalar tensor (one element).
|
||||
silu_mul: Treat the output as ``[gate | up]`` halves and return the
|
||||
BF16 ``silu(gate) * up`` (``[M, N / 2]``) of the BF16 gate / up
|
||||
outputs (exact SiLU, one BF16 rounding).
|
||||
pdl: Launch with programmatic dependent launch: weight loads start
|
||||
while the previous kernel drains. Disable after kernels that need
|
||||
the whole GPU to make progress (e.g. attention), whose remaining
|
||||
CTAs the early, waiting GEMV CTAs would starve.
|
||||
|
||||
Returns:
|
||||
The BF16 ``[M, N]`` (``[M, N / 2]`` with ``silu_mul``) output.
|
||||
|
||||
"""
|
||||
assert weight.shape[1] * 2 == x.shape[1]
|
||||
return _launch(
|
||||
x,
|
||||
weight.view(-1),
|
||||
weight_scale.view(torch.uint8).view(-1),
|
||||
global_scale.reshape(1),
|
||||
weight.shape[0],
|
||||
silu_mul,
|
||||
pdl,
|
||||
bf16=False,
|
||||
)
|
||||
|
||||
|
||||
def bf16_gemv(
|
||||
x: torch.Tensor,
|
||||
weight: torch.Tensor,
|
||||
silu_mul: bool = False,
|
||||
pdl: bool = True,
|
||||
) -> torch.Tensor:
|
||||
"""Small-M BF16 GEMM ``x @ weight.T`` (FP32 accumulate); same kernel and
|
||||
arguments as ``nvfp4_gemv`` with a row-major BF16 ``[N, K]`` weight."""
|
||||
assert weight.dtype == torch.bfloat16 and weight.is_contiguous()
|
||||
assert weight.shape[1] == x.shape[1]
|
||||
return _launch(
|
||||
x,
|
||||
weight.view(torch.uint8).view(-1),
|
||||
weight.view(torch.uint8).view(-1),
|
||||
_one(x.device),
|
||||
weight.shape[0],
|
||||
silu_mul,
|
||||
pdl,
|
||||
bf16=True,
|
||||
)
|
||||
|
||||
|
||||
_ONES: dict[torch.device, torch.Tensor] = {}
|
||||
|
||||
|
||||
def _one(device: torch.device) -> torch.Tensor:
|
||||
one = _ONES.get(device)
|
||||
if one is None:
|
||||
one = _ONES[device] = torch.ones(1, dtype=torch.float32, device=device)
|
||||
return one
|
||||
|
||||
|
||||
def warmup(
|
||||
n: int,
|
||||
k: int,
|
||||
silu_mul: bool = False,
|
||||
pdl: bool = True,
|
||||
bf16: bool = False,
|
||||
) -> None:
|
||||
"""Compile every variant the GEMV may pick for this shape."""
|
||||
for nt in _configs(n, k, silu_mul, bf16):
|
||||
_get_compiled(n, k, nt, silu_mul, pdl, bf16)
|
||||
|
||||
|
||||
def _dequant_kernel():
|
||||
import triton
|
||||
import triton.language as tl
|
||||
|
||||
@triton.jit
|
||||
def kernel(
|
||||
w_ptr, s_ptr, out_ptr, K: tl.constexpr, BR: tl.constexpr, BK: tl.constexpr
|
||||
):
|
||||
r = tl.program_id(0) * BR + tl.arange(0, BR)
|
||||
kb = tl.program_id(1) * BK
|
||||
b = tl.load(
|
||||
w_ptr
|
||||
+ r[:, None].to(tl.int64) * (K // 2)
|
||||
+ (kb // 2 + tl.arange(0, BK // 2))[None, :]
|
||||
)
|
||||
# fp4 bits -> fp32 bits of e2m1 * 2^-126 (exact), sign included.
|
||||
lo = ((b & 7).to(tl.int32) << 22) | ((b & 8).to(tl.int32) << 28)
|
||||
hi = ((b & 0x70).to(tl.int32) << 18) | ((b & 0x80).to(tl.int32) << 24)
|
||||
v = tl.reshape(
|
||||
tl.interleave(lo, hi).to(tl.float32, bitcast=True), (BR, BK // 16, 16)
|
||||
)
|
||||
g = kb // 16 + tl.arange(0, BK // 16)
|
||||
sidx = ((r[:, None] // 128) * (K // 64) + g[None, :] // 4).to(tl.int64) * 512
|
||||
sidx += ((r % 32) * 16 + ((r % 128) // 32) * 4)[:, None] + g[None, :] % 4
|
||||
sc = tl.load(s_ptr + sidx).to(tl.float32)
|
||||
v = tl.reshape(v * 8.507059173023462e37 * sc[:, :, None], (BR, BK))
|
||||
out = out_ptr + r[:, None].to(tl.int64) * K + (kb + tl.arange(0, BK))[None, :]
|
||||
tl.store(out, v.to(tl.bfloat16))
|
||||
|
||||
return kernel
|
||||
|
||||
|
||||
_dequant = None
|
||||
|
||||
|
||||
def dequantize_rows(weight: torch.Tensor, weight_scale: torch.Tensor) -> torch.Tensor:
|
||||
"""Exact BF16 ``e2m1 * e4m3`` of all weight rows (global scale not
|
||||
applied); ``weight_scale`` is 128x4-swizzled."""
|
||||
global _dequant
|
||||
if _dequant is None:
|
||||
_dequant = _dequant_kernel()
|
||||
rows, k = weight.shape[0], weight.shape[1] * 2
|
||||
assert rows % 16 == 0 and k % 256 == 0
|
||||
out = torch.empty(rows, k, dtype=torch.bfloat16, device=weight.device)
|
||||
_dequant[(rows // 16, k // 256)](
|
||||
weight,
|
||||
weight_scale.view(torch.float8_e4m3fn).view(-1),
|
||||
out,
|
||||
k,
|
||||
16,
|
||||
256,
|
||||
num_warps=4,
|
||||
)
|
||||
return out
|
||||
@@ -0,0 +1,187 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Fused per-head q/k RMSNorm + NeoX RoPE + paged KV-cache write.
|
||||
|
||||
Numerics follow the unfused vLLM ops it replaces: RMSNorm computes
|
||||
``bf16(x * rsqrt(mean(x^2) + eps) * w)`` in fp32, RoPE computes
|
||||
``x * cos - y * sin`` in fp32 from the fp32 cos/sin cache (as the reference
|
||||
implementation does; vLLM's rotary op would round the cache to bf16 first) and
|
||||
rounds to bf16, and K/V are stored unchanged in the bf16 cache.
|
||||
"""
|
||||
|
||||
import torch
|
||||
|
||||
from vllm.triton_utils import tl, triton
|
||||
|
||||
_BLOCK_T = 16
|
||||
|
||||
|
||||
# A fixed token tile and no integer specialization: one compiled variant per
|
||||
# (RoPE, PDL) pair, all of which CUDA graph capture and warmup reach.
|
||||
@triton.jit(do_not_specialize=["num_tokens", "num_slots"])
|
||||
def _qk_norm_rope_cache_kernel(
|
||||
qkv_ptr,
|
||||
qkv_stride,
|
||||
q_out_ptr,
|
||||
positions_ptr,
|
||||
q_weight_ptr,
|
||||
k_weight_ptr,
|
||||
cos_sin_ptr,
|
||||
cos_sin_stride,
|
||||
slot_mapping_ptr,
|
||||
num_tokens,
|
||||
num_slots,
|
||||
k_cache_ptr,
|
||||
v_cache_ptr,
|
||||
stride_block,
|
||||
stride_slot,
|
||||
stride_head,
|
||||
block_size,
|
||||
eps,
|
||||
NUM_Q_HEADS: tl.constexpr,
|
||||
NUM_KV_HEADS: tl.constexpr,
|
||||
HEAD_DIM: tl.constexpr,
|
||||
APPLY_ROPE: tl.constexpr,
|
||||
BLOCK_T: tl.constexpr,
|
||||
LAUNCH_DEPENDENTS: tl.constexpr,
|
||||
):
|
||||
if LAUNCH_DEPENDENTS:
|
||||
# The PDL successor waits on this grid before reading q / K / V.
|
||||
tl.extra.cuda.gdc_launch_dependents()
|
||||
head = tl.program_id(1)
|
||||
half: tl.constexpr = HEAD_DIM // 2
|
||||
tokens = tl.program_id(0) * BLOCK_T + tl.arange(0, BLOCK_T)
|
||||
offs = tl.arange(0, half)[None, :]
|
||||
t_mask = (tokens < num_tokens)[:, None]
|
||||
tok64 = tokens.to(tl.int64)[:, None]
|
||||
row = qkv_ptr + tok64 * qkv_stride + head * HEAD_DIM
|
||||
x1 = tl.load(row + offs, mask=t_mask, other=0.0).to(tl.float32)
|
||||
x2 = tl.load(row + half + offs, mask=t_mask, other=0.0).to(tl.float32)
|
||||
|
||||
is_q = head < NUM_Q_HEADS
|
||||
is_v = head >= NUM_Q_HEADS + NUM_KV_HEADS
|
||||
is_k = (head >= NUM_Q_HEADS) & (head < NUM_Q_HEADS + NUM_KV_HEADS)
|
||||
w_ptr = tl.where(is_q, q_weight_ptr, k_weight_ptr)
|
||||
w1 = tl.load(w_ptr + offs).to(tl.float32)
|
||||
w2 = tl.load(w_ptr + half + offs).to(tl.float32)
|
||||
var = (tl.sum(x1 * x1, axis=1) + tl.sum(x2 * x2, axis=1)) / HEAD_DIM
|
||||
rs = tl.math.rsqrt(var + eps)[:, None]
|
||||
n1 = (x1 * rs * w1).to(tl.bfloat16).to(tl.float32)
|
||||
n2 = (x2 * rs * w2).to(tl.bfloat16).to(tl.float32)
|
||||
if APPLY_ROPE:
|
||||
pos = tl.load(positions_ptr + tokens, mask=tokens < num_tokens, other=0)
|
||||
cs = cos_sin_ptr + pos.to(tl.int64)[:, None] * cos_sin_stride
|
||||
cos = tl.load(cs + offs, mask=t_mask, other=0.0).to(tl.float32)
|
||||
sin = tl.load(cs + half + offs, mask=t_mask, other=0.0).to(tl.float32)
|
||||
r1 = n1 * cos - n2 * sin
|
||||
r2 = n2 * cos + n1 * sin
|
||||
n1 = r1
|
||||
n2 = r2
|
||||
o1 = tl.where(is_v, x1, n1).to(tl.bfloat16)
|
||||
o2 = tl.where(is_v, x2, n2).to(tl.bfloat16)
|
||||
|
||||
out = q_out_ptr + (tok64 * NUM_Q_HEADS + head) * HEAD_DIM
|
||||
tl.store(out + offs, o1, mask=t_mask & is_q)
|
||||
tl.store(out + half + offs, o2, mask=t_mask & is_q)
|
||||
# Keep the normalized key in qkv as well; attention may read it.
|
||||
tl.store(row + offs, o1, mask=t_mask & is_k)
|
||||
tl.store(row + half + offs, o2, mask=t_mask & is_k)
|
||||
|
||||
slot = tl.load(slot_mapping_ptr + tokens, mask=tokens < num_slots, other=-1)
|
||||
slot = slot.to(tl.int64)[:, None]
|
||||
kv_head = head - NUM_Q_HEADS - tl.where(is_v, NUM_KV_HEADS, 0)
|
||||
dst = (
|
||||
(slot // block_size) * stride_block
|
||||
+ (slot % block_size) * stride_slot
|
||||
+ kv_head * stride_head
|
||||
)
|
||||
k_mask = (slot >= 0) & is_k
|
||||
v_mask = (slot >= 0) & is_v
|
||||
tl.store(k_cache_ptr + dst + offs, o1, mask=k_mask)
|
||||
tl.store(k_cache_ptr + dst + half + offs, o2, mask=k_mask)
|
||||
tl.store(v_cache_ptr + dst + offs, o1, mask=v_mask)
|
||||
tl.store(v_cache_ptr + dst + half + offs, o2, mask=v_mask)
|
||||
|
||||
|
||||
def qk_norm_rope_cache(
|
||||
qkv: torch.Tensor,
|
||||
positions: torch.Tensor,
|
||||
q_weight: torch.Tensor,
|
||||
k_weight: torch.Tensor,
|
||||
eps: float,
|
||||
cos_sin_cache: torch.Tensor | None,
|
||||
k_cache: torch.Tensor | None,
|
||||
v_cache: torch.Tensor | None,
|
||||
slot_mapping: torch.Tensor | None,
|
||||
num_q_heads: int,
|
||||
num_kv_heads: int,
|
||||
head_dim: int,
|
||||
launch_dependents: bool = False,
|
||||
) -> torch.Tensor:
|
||||
"""Normalize and rotate q/k from ``qkv`` and write k/v into the cache.
|
||||
|
||||
Args:
|
||||
qkv: [T, (Hq + 2 * Hkv) * D] bf16; k is normalized in place.
|
||||
positions: [T] token positions.
|
||||
q_weight: [D] q RMSNorm weight.
|
||||
k_weight: [D] k RMSNorm weight.
|
||||
eps: RMSNorm epsilon.
|
||||
cos_sin_cache: [max_pos, D] fp32 (cos | sin), or None for no RoPE.
|
||||
k_cache: [num_blocks, block_size, Hkv, D] view (any strides with a
|
||||
contiguous last dim), or None to skip the cache write.
|
||||
v_cache: Same shape and strides as ``k_cache``.
|
||||
slot_mapping: [<= T] int64; negative slots are skipped.
|
||||
num_q_heads: Hq.
|
||||
num_kv_heads: Hkv.
|
||||
head_dim: D.
|
||||
launch_dependents: Let a PDL successor that waits on the grid
|
||||
dependency before reading q / K / V (Flint decode attention) start
|
||||
its prologue early. Must stay False before other kernels.
|
||||
|
||||
Returns:
|
||||
q: [T, Hq * D] bf16.
|
||||
|
||||
"""
|
||||
num_tokens = qkv.shape[0]
|
||||
q = torch.empty(
|
||||
(num_tokens, num_q_heads * head_dim), dtype=qkv.dtype, device=qkv.device
|
||||
)
|
||||
if num_tokens == 0:
|
||||
return q
|
||||
if k_cache is not None and v_cache is not None and slot_mapping is not None:
|
||||
assert k_cache.stride() == v_cache.stride() and k_cache.stride(-1) == 1
|
||||
strides = k_cache.stride()[:3]
|
||||
block_size = k_cache.shape[1]
|
||||
num_slots = slot_mapping.shape[0]
|
||||
else:
|
||||
k_cache = v_cache = slot_mapping = qkv
|
||||
strides = (0, 0, 0)
|
||||
block_size = 1
|
||||
num_slots = 0
|
||||
grid = (triton.cdiv(num_tokens, _BLOCK_T), num_q_heads + 2 * num_kv_heads)
|
||||
_qk_norm_rope_cache_kernel[grid](
|
||||
qkv,
|
||||
qkv.stride(0),
|
||||
q,
|
||||
positions,
|
||||
q_weight,
|
||||
k_weight,
|
||||
qkv if cos_sin_cache is None else cos_sin_cache,
|
||||
0 if cos_sin_cache is None else cos_sin_cache.stride(0),
|
||||
slot_mapping,
|
||||
num_tokens,
|
||||
num_slots,
|
||||
k_cache,
|
||||
v_cache,
|
||||
*strides,
|
||||
block_size,
|
||||
eps,
|
||||
NUM_Q_HEADS=num_q_heads,
|
||||
NUM_KV_HEADS=num_kv_heads,
|
||||
HEAD_DIM=head_dim,
|
||||
APPLY_ROPE=cos_sin_cache is not None,
|
||||
BLOCK_T=_BLOCK_T,
|
||||
LAUNCH_DEPENDENTS=launch_dependents,
|
||||
num_warps=4,
|
||||
)
|
||||
return q
|
||||
@@ -0,0 +1,583 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Fused decode qkv for Flint at M <= 8 tokens (CuTe DSL), two kernels:
|
||||
|
||||
1. ``fused_add_rms_norm`` (or ``rms_norm`` for the first layer) writing the
|
||||
normalized activations ``xn`` and the new residual; it releases its PDL
|
||||
dependents immediately so the second kernel's weights stream in meanwhile.
|
||||
2. the qkv GEMV (same fragment math, K split and summation order as
|
||||
``nvfp4_gemv`` at M <= 8) whose 16-row CTAs form one cluster per head and
|
||||
send their BF16 rows to rank 0 through DSMEM, which applies per-head q/k
|
||||
RMSNorm, NeoX RoPE (FP32 cos/sin table, FP32 math) and the paged KV-cache
|
||||
write like ``qk_norm_rope_cache``.
|
||||
|
||||
Numerics are those of ``norm.fused_add_rms_norm`` / ``norm.rms_norm`` ->
|
||||
GEMV -> ``qk_norm_rope_cache``; the per-head q/k norm sums its squares in a
|
||||
different order, so q/k can differ from that path by one BF16 ulp.
|
||||
"""
|
||||
|
||||
from functools import cache
|
||||
|
||||
import torch
|
||||
|
||||
from .nvfp4_gemv import _one
|
||||
|
||||
_K = 2048
|
||||
_HEAD = 128
|
||||
_CHUNK = 64
|
||||
_WARPS = 8
|
||||
MAX_M = 8
|
||||
|
||||
|
||||
@cache
|
||||
def _kernels():
|
||||
import cutlass
|
||||
import cutlass.cute as cute
|
||||
from cutlass import Float32, Int32, Int64, Uint32, const_expr
|
||||
from cutlass.cutlass_dsl import dsl_user_op
|
||||
|
||||
from .cute_primitives import (
|
||||
bf16x2,
|
||||
decode,
|
||||
fma,
|
||||
ld_u8,
|
||||
ld_w8,
|
||||
ld_w16,
|
||||
ld_x16,
|
||||
lds_f32,
|
||||
mma,
|
||||
ptx,
|
||||
round_bf16,
|
||||
scale_pair,
|
||||
smem_addr,
|
||||
stg_bf16,
|
||||
stg_u32x4,
|
||||
sts_f32,
|
||||
)
|
||||
|
||||
@dsl_user_op
|
||||
def ldg128(addr, *, loc=None, ip=None):
|
||||
return ptx(
|
||||
"ld.global.v4.b32 {$0,$1,$2,$3}, [$4];",
|
||||
"=r,=r,=r,=r,l",
|
||||
[Int64(addr)],
|
||||
(Uint32,) * 4,
|
||||
loc=loc,
|
||||
ip=ip,
|
||||
)
|
||||
|
||||
@dsl_user_op
|
||||
def ldg128_nc(addr, *, loc=None, ip=None):
|
||||
return ptx(
|
||||
"ld.global.nc.v4.b32 {$0,$1,$2,$3}, [$4];",
|
||||
"=r,=r,=r,=r,l",
|
||||
[Int64(addr)],
|
||||
(Uint32,) * 4,
|
||||
effects=False,
|
||||
loc=loc,
|
||||
ip=ip,
|
||||
)
|
||||
|
||||
@dsl_user_op
|
||||
def unpack(w, *, loc=None, ip=None):
|
||||
return ptx(
|
||||
"{ .reg .b16 l, h; mov.b32 {l, h}, $2; cvt.f32.bf16 $0, l; "
|
||||
"cvt.f32.bf16 $1, h; }",
|
||||
"=f,=f,r",
|
||||
[Uint32(w)],
|
||||
(Float32, Float32),
|
||||
effects=False,
|
||||
loc=loc,
|
||||
ip=ip,
|
||||
)
|
||||
|
||||
@dsl_user_op
|
||||
def rsqrt(x, *, loc=None, ip=None):
|
||||
return ptx(
|
||||
"rsqrt.approx.ftz.f32 $0, $1;",
|
||||
"=f,f",
|
||||
[Float32(x)],
|
||||
Float32,
|
||||
effects=False,
|
||||
loc=loc,
|
||||
ip=ip,
|
||||
)
|
||||
|
||||
@dsl_user_op
|
||||
def ld_bf16(addr, *, loc=None, ip=None):
|
||||
return ptx(
|
||||
"{ .reg .b16 h; ld.global.nc.b16 h, [$1]; cvt.f32.bf16 $0, h; }",
|
||||
"=f,l",
|
||||
[Int64(addr)],
|
||||
Float32,
|
||||
effects=False,
|
||||
loc=loc,
|
||||
ip=ip,
|
||||
)
|
||||
|
||||
@dsl_user_op
|
||||
def ld_f32(addr, *, loc=None, ip=None):
|
||||
return ptx(
|
||||
"ld.global.nc.f32 $0, [$1];",
|
||||
"=f,l",
|
||||
[Int64(addr)],
|
||||
Float32,
|
||||
effects=False,
|
||||
loc=loc,
|
||||
ip=ip,
|
||||
)
|
||||
|
||||
@dsl_user_op
|
||||
def ld_i64(addr, *, loc=None, ip=None):
|
||||
return ptx(
|
||||
"ld.global.nc.s64 $0, [$1];",
|
||||
"=l,l",
|
||||
[Int64(addr)],
|
||||
Int64,
|
||||
effects=False,
|
||||
loc=loc,
|
||||
ip=ip,
|
||||
)
|
||||
|
||||
@dsl_user_op
|
||||
def ld_i64_pred(addr, pred, *, loc=None, ip=None):
|
||||
return ptx(
|
||||
"{ .reg .pred p; setp.ne.b32 p, $2, 0; mov.b64 $0, -1; "
|
||||
"@p ld.global.nc.s64 $0, [$1]; }",
|
||||
"=l,l,r",
|
||||
[Int64(addr), Int32(pred)],
|
||||
Int64,
|
||||
effects=False,
|
||||
loc=loc,
|
||||
ip=ip,
|
||||
)
|
||||
|
||||
@dsl_user_op
|
||||
def st_cluster0_f32(addr, v, *, loc=None, ip=None):
|
||||
ptx(
|
||||
"{ .reg .b32 r; mapa.shared::cluster.u32 r, $0, 0; "
|
||||
"st.shared::cluster.f32 [r], $1; }",
|
||||
"r,f,~{memory}",
|
||||
[Uint32(addr), Float32(v)],
|
||||
loc=loc,
|
||||
ip=ip,
|
||||
)
|
||||
|
||||
class Norm:
|
||||
"""Grid (M,): one CTA of 256 threads per token row, thread i owns
|
||||
elements 8i .. 8i + 7."""
|
||||
|
||||
def __init__(self, has_residual):
|
||||
self.has_residual = has_residual
|
||||
|
||||
@cute.jit
|
||||
def __call__(self, x, res_in, res_out, xn, norm_w, M: Int32, eps: Float32,
|
||||
stream): # fmt: skip
|
||||
self.kernel(x, res_in, res_out, xn, norm_w, eps).launch(
|
||||
grid=[M, 1, 1], block=[256, 1, 1], stream=stream, use_pdl=True
|
||||
)
|
||||
|
||||
@cute.kernel
|
||||
def kernel(self, x, res_in, res_out, xn, norm_w, eps: Float32):
|
||||
tidx, _, _ = cute.arch.thread_idx()
|
||||
tok, _, _ = cute.arch.block_idx()
|
||||
warp = tidx // 32
|
||||
lane = tidx % 32
|
||||
smem = cutlass.memory.SmemAllocator()
|
||||
var_ptr = smem.allocate_array(Float32, 8, byte_alignment=16)
|
||||
varb = smem_addr(var_ptr)
|
||||
nwv = ldg128_nc(norm_w.iterator.toint() + tidx * 16)
|
||||
cute.arch.griddepcontrol_wait()
|
||||
# The GEMV only needs our output after its own grid-dependency
|
||||
# wait: let it launch and stream its weights now.
|
||||
cute.arch.griddepcontrol_launch_dependents()
|
||||
off = Int64(tok) * (_K * 2) + tidx * 16
|
||||
# s = x (+ residual) in fp32; the variance and the output use the
|
||||
# unrounded s, the new residual is bf16(s).
|
||||
hv = ldg128(x.iterator.toint() + off)
|
||||
sv = [v for i in range(4) for v in unpack(hv[i])]
|
||||
if const_expr(self.has_residual):
|
||||
rv = ldg128(res_in.iterator.toint() + off)
|
||||
rf = [v for i in range(4) for v in unpack(rv[i])]
|
||||
sv = [sv[j] + rf[j] for j in range(8)]
|
||||
stg_u32x4(
|
||||
res_out.iterator.toint() + off,
|
||||
*[bf16x2(sv[2 * i], sv[2 * i + 1]) for i in range(4)],
|
||||
)
|
||||
acc_s = Float32(0.0)
|
||||
for i in cutlass.range_constexpr(4):
|
||||
acc_s = acc_s + sv[2 * i] * sv[2 * i] + sv[2 * i + 1] * sv[2 * i + 1]
|
||||
for o in cutlass.range_constexpr(5):
|
||||
acc_s = acc_s + cute.arch.shuffle_sync_bfly(acc_s, 1 << o)
|
||||
if lane == 0:
|
||||
sts_f32(varb + warp * 4, acc_s)
|
||||
cute.arch.sync_threads()
|
||||
v = Float32(0.0)
|
||||
for w2 in cutlass.range_constexpr(_WARPS):
|
||||
v = v + lds_f32(varb + w2 * 4)
|
||||
rs = rsqrt(v / Float32(_K) + eps)
|
||||
outw = []
|
||||
for i in cutlass.range_constexpr(4):
|
||||
wlo, whi = unpack(nwv[i])
|
||||
outw.append(bf16x2(sv[2 * i] * rs * wlo, sv[2 * i + 1] * rs * whi))
|
||||
stg_u32x4(xn.iterator.toint() + off, *outw)
|
||||
|
||||
class QkvRope:
|
||||
"""Grid (N / 16) CTAs in clusters of 8 (one 128-row head each)."""
|
||||
|
||||
def __init__(self, bf16w, rope, num_q_heads, num_kv_heads):
|
||||
self.bf16w = bf16w
|
||||
self.rope = rope
|
||||
self.hq = num_q_heads
|
||||
self.hkv = num_kv_heads
|
||||
|
||||
@cute.jit
|
||||
def __call__(self, xn, w, s, gs, qn_w, kn_w, cos_sin, positions, slots,
|
||||
q_out, k_cache, v_cache, M: Int32, num_slots: Int32,
|
||||
qk_eps: Float32, stride_block: Int64, stride_slot: Int64,
|
||||
stride_head: Int64, block_size: Int32, stream): # fmt: skip
|
||||
n = (self.hq + 2 * self.hkv) * _HEAD
|
||||
self.kernel(
|
||||
xn, w, s, gs, qn_w, kn_w, cos_sin, positions, slots, q_out,
|
||||
k_cache, v_cache, M, num_slots, qk_eps, stride_block,
|
||||
stride_slot, stride_head, block_size,
|
||||
).launch(
|
||||
grid=[n // 16, 1, 1],
|
||||
block=[32 * _WARPS, 1, 1],
|
||||
cluster=[8, 1, 1],
|
||||
stream=stream,
|
||||
use_pdl=True,
|
||||
) # fmt: skip
|
||||
|
||||
@cute.kernel
|
||||
def kernel(self, xn, w, s, gs, qn_w, kn_w, cos_sin, positions, slots,
|
||||
q_out, k_cache, v_cache, M: Int32, num_slots: Int32,
|
||||
qk_eps: Float32, stride_block: Int64, stride_slot: Int64,
|
||||
stride_head: Int64, block_size: Int32): # fmt: skip
|
||||
BF16W: cutlass.Constexpr = self.bf16w
|
||||
KC: cutlass.Constexpr = _K // _CHUNK
|
||||
CPW: cutlass.Constexpr = KC // _WARPS
|
||||
NACC: cutlass.Constexpr = 8
|
||||
KB: cutlass.Constexpr = KC * (128 if self.bf16w else 32)
|
||||
QW: cutlass.Constexpr = 16 if self.bf16w else 4
|
||||
tidx, _, _ = cute.arch.thread_idx()
|
||||
nb, _, _ = cute.arch.block_idx()
|
||||
head = nb // 8
|
||||
rank = nb % 8
|
||||
warp = tidx // 32
|
||||
lane = tidx % 32
|
||||
g = lane // 4
|
||||
t = lane % 4
|
||||
c_begin = warp * CPW
|
||||
|
||||
smem = cutlass.memory.SmemAllocator()
|
||||
red_ptr = smem.allocate_array(Float32, _WARPS * 4 * 32, byte_alignment=16)
|
||||
red = smem_addr(red_ptr)
|
||||
hb_ptr = smem.allocate_array(Float32, _HEAD * 8, byte_alignment=16)
|
||||
hb = smem_addr(hb_ptr)
|
||||
|
||||
# ---- weights and epilogue inputs (before the grid dependency) ----
|
||||
qs = cute.make_rmem_tensor((CPW * QW,), Uint32)
|
||||
ss = cute.make_rmem_tensor((CPW * 2,), Uint32)
|
||||
row_g = nb * 16 + g
|
||||
row_g8 = row_g + 8
|
||||
if const_expr(BF16W):
|
||||
wbase = w.iterator.toint() + Int64(row_g) * KB + c_begin * 128 + t * 32
|
||||
for c in cutlass.range_constexpr(CPW):
|
||||
for h in cutlass.range_constexpr(4):
|
||||
off = (h // 2) * 8 * KB + c * 128 + (h % 2) * 16
|
||||
wv = ld_w16(wbase + off)
|
||||
for i in cutlass.range_constexpr(4):
|
||||
qs[c * QW + h * 4 + i] = wv[i]
|
||||
else:
|
||||
wbase = w.iterator.toint() + Int64(row_g) * KB + c_begin * 32 + t * 8
|
||||
sbase_g = (
|
||||
s.iterator.toint()
|
||||
+ Int64(row_g // 128) * (KC * 512)
|
||||
+ (row_g % 32) * 16
|
||||
+ ((row_g % 128) // 32) * 4
|
||||
+ t
|
||||
+ c_begin * 512
|
||||
)
|
||||
sbase_g8 = (
|
||||
s.iterator.toint()
|
||||
+ Int64(row_g8 // 128) * (KC * 512)
|
||||
+ (row_g8 % 32) * 16
|
||||
+ ((row_g8 % 128) // 32) * 4
|
||||
+ t
|
||||
+ c_begin * 512
|
||||
)
|
||||
for c in cutlass.range_constexpr(CPW):
|
||||
qa = ld_w8(wbase + c * 32)
|
||||
qb = ld_w8(wbase + 8 * KB + c * 32)
|
||||
qs[c * QW + 0] = qa[0]
|
||||
qs[c * QW + 1] = qa[1]
|
||||
qs[c * QW + 2] = qb[0]
|
||||
qs[c * QW + 3] = qb[1]
|
||||
ss[c * 2] = ld_u8(sbase_g + c * 512)
|
||||
ss[c * 2 + 1] = ld_u8(sbase_g8 + c * 512)
|
||||
gsv = Float32(1.0)
|
||||
if const_expr(not BF16W):
|
||||
gsv = gs[0]
|
||||
is_q = head < self.hq
|
||||
is_v = head >= self.hq + self.hkv
|
||||
wptr = cutlass.select_(is_q, qn_w.iterator.toint(), kn_w.iterator.toint())
|
||||
hw = [ld_bf16(Int64(wptr) + (lane + 32 * e) * 2) for e in range(4)]
|
||||
etc = cutlass.min(warp, M - 1)
|
||||
pos = Int64(0)
|
||||
if const_expr(self.rope):
|
||||
pos = ld_i64(positions.iterator.toint() + Int64(etc) * 8)
|
||||
slot = ld_i64_pred(
|
||||
slots.iterator.toint() + Int64(etc) * 8, Int32(etc < num_slots)
|
||||
)
|
||||
cs_v = [Float32(1.0)] * 2
|
||||
sn_v = [Float32(0.0)] * 2
|
||||
if const_expr(self.rope):
|
||||
# FP32 cos | sin table rows of 128 floats.
|
||||
cbase = cos_sin.iterator.toint() + pos * (_HEAD * 4)
|
||||
for e in cutlass.range_constexpr(2):
|
||||
cs_v[e] = ld_f32(cbase + (lane + 32 * e) * 4)
|
||||
sn_v[e] = ld_f32(cbase + (64 + lane + 32 * e) * 4)
|
||||
cute.arch.cluster_arrive_relaxed()
|
||||
|
||||
cute.arch.griddepcontrol_wait()
|
||||
xbase = xn.iterator.toint() + (Int64(c_begin) * _CHUNK + t * 16) * 2
|
||||
xall = [
|
||||
ld_x16(xbase + Int64(g) * (_K * 2) + c * 128, Int32(g < M))
|
||||
for c in range(CPW)
|
||||
]
|
||||
|
||||
# ---- GEMV ----
|
||||
acc = cute.make_rmem_tensor((NACC * 4,), Float32)
|
||||
acc.fill(0.0)
|
||||
for c in cutlass.range_constexpr(CPW):
|
||||
xv = xall[c]
|
||||
q0 = c * QW
|
||||
if const_expr(not BF16W):
|
||||
sg = scale_pair(ss[c * 2])
|
||||
sg8 = scale_pair(ss[c * 2 + 1])
|
||||
for j in cutlass.range_constexpr(4):
|
||||
if const_expr(BF16W):
|
||||
a = [
|
||||
qs[q0 + 2 * j],
|
||||
qs[q0 + 8 + 2 * j],
|
||||
qs[q0 + 2 * j + 1],
|
||||
qs[q0 + 8 + 2 * j + 1],
|
||||
]
|
||||
else:
|
||||
a = decode(qs[q0 + j // 2], qs[q0 + 2 + j // 2], j, sg, sg8)
|
||||
ab = ((c * 4 + j) % NACC) * 4
|
||||
r = mma(
|
||||
[acc[ab], acc[ab + 1], acc[ab + 2], acc[ab + 3]],
|
||||
a,
|
||||
xv[2 * j],
|
||||
xv[2 * j + 1],
|
||||
)
|
||||
for i in cutlass.range_constexpr(4):
|
||||
acc[ab + i] = r[i]
|
||||
for h in cutlass.range_constexpr(1, NACC):
|
||||
for i in cutlass.range_constexpr(4):
|
||||
acc[i] = acc[i] + acc[h * 4 + i]
|
||||
cute.arch.griddepcontrol_launch_dependents()
|
||||
for i in cutlass.range_constexpr(4):
|
||||
sts_f32(red + ((warp * 4 + i) * 32 + lane) * 4, acc[i])
|
||||
cute.arch.sync_threads()
|
||||
cute.arch.cluster_wait()
|
||||
if warp == 0:
|
||||
for i in cutlass.range_constexpr(4):
|
||||
v = acc[i]
|
||||
for w2 in cutlass.range_constexpr(1, _WARPS):
|
||||
v = v + lds_f32(red + ((w2 * 4 + i) * 32 + lane) * 4)
|
||||
v = round_bf16(v * gsv)
|
||||
row = rank * 16 + g + 8 * (i // 2)
|
||||
tok = 2 * t + (i % 2)
|
||||
st_cluster0_f32(hb + (row * 8 + tok) * 4, v)
|
||||
cute.arch.cluster_arrive()
|
||||
cute.arch.cluster_wait()
|
||||
|
||||
# ---- per-head epilogue on rank 0: warp = token, lane covers rows
|
||||
# lane + 32 * r; RoPE pairs (lane, lane + 64), (lane + 32, lane + 96).
|
||||
if (rank == 0) & (warp < M):
|
||||
y = [lds_f32(hb + ((lane + 32 * e) * 8 + warp) * 4) for e in range(4)]
|
||||
sq = y[0] * y[0] + y[1] * y[1] + y[2] * y[2] + y[3] * y[3]
|
||||
for o in cutlass.range_constexpr(5):
|
||||
sq = sq + cute.arch.shuffle_sync_bfly(sq, 1 << o)
|
||||
rs = rsqrt(sq / Float32(_HEAD) + qk_eps)
|
||||
nv = [round_bf16(y[e] * rs * hw[e]) for e in range(4)]
|
||||
if const_expr(self.rope):
|
||||
rot = [None] * 4
|
||||
for e in cutlass.range_constexpr(2):
|
||||
rot[e] = fma(nv[e], cs_v[e], -(nv[e + 2] * sn_v[e]))
|
||||
rot[e + 2] = fma(nv[e + 2], cs_v[e], nv[e] * sn_v[e])
|
||||
nv = rot
|
||||
out = [cutlass.select_(is_v, y[e], nv[e]) for e in range(4)]
|
||||
if is_q:
|
||||
qrow = (
|
||||
q_out.iterator.toint()
|
||||
+ (Int64(warp) * (self.hq * _HEAD) + head * _HEAD) * 2
|
||||
)
|
||||
for e in cutlass.range_constexpr(4):
|
||||
stg_bf16(qrow + (lane + 32 * e) * 2, out[e])
|
||||
elif slot >= 0:
|
||||
kvh = cutlass.select_(
|
||||
is_v, head - self.hq - self.hkv, head - self.hq
|
||||
)
|
||||
base = cutlass.select_(
|
||||
is_v, v_cache.iterator.toint(), k_cache.iterator.toint()
|
||||
)
|
||||
bs = Int64(block_size)
|
||||
dst = (
|
||||
Int64(base)
|
||||
+ (
|
||||
(slot // bs) * stride_block
|
||||
+ (slot % bs) * stride_slot
|
||||
+ Int64(kvh) * stride_head
|
||||
)
|
||||
* 2
|
||||
)
|
||||
for e in cutlass.range_constexpr(4):
|
||||
stg_bf16(dst + (lane + 32 * e) * 2, out[e])
|
||||
|
||||
return Norm, QkvRope
|
||||
|
||||
|
||||
@cache
|
||||
def _compile_norm(has_residual):
|
||||
import cutlass
|
||||
import cutlass.cute as cute
|
||||
from cutlass import Float32, Int32
|
||||
from quack.compile_utils import make_fake_tensor
|
||||
|
||||
sym = cute.sym_int
|
||||
bf16 = cutlass.BFloat16
|
||||
args = [make_fake_tensor(bf16, (sym(), _K), divisibility=8) for _ in range(4)]
|
||||
args.append(make_fake_tensor(bf16, (_K,), divisibility=8))
|
||||
stream = cute.runtime.make_fake_stream(use_tvm_ffi_env_stream=True)
|
||||
return cute.compile(
|
||||
_kernels()[0](has_residual), *args, Int32(1), Float32(1e-6), stream,
|
||||
options="--enable-tvm-ffi",
|
||||
) # fmt: skip
|
||||
|
||||
|
||||
@cache
|
||||
def _compile_qkv(bf16w, rope, num_q_heads, num_kv_heads):
|
||||
import cutlass
|
||||
import cutlass.cute as cute
|
||||
from cutlass import Float32, Int32, Int64
|
||||
from quack.compile_utils import make_fake_tensor
|
||||
|
||||
sym = cute.sym_int
|
||||
bf16 = cutlass.BFloat16
|
||||
args = [
|
||||
make_fake_tensor(bf16, (sym(), _K), divisibility=8), # xn
|
||||
make_fake_tensor(cutlass.Uint8, (sym(),), divisibility=16), # weight
|
||||
make_fake_tensor(cutlass.Uint8, (sym(),), divisibility=4), # scales
|
||||
make_fake_tensor(Float32, (1,), divisibility=1), # global scale
|
||||
make_fake_tensor(bf16, (_HEAD,), divisibility=8), # q norm w
|
||||
make_fake_tensor(bf16, (_HEAD,), divisibility=8), # k norm w
|
||||
make_fake_tensor(Float32, (sym(), sym()), divisibility=4), # cos | sin
|
||||
make_fake_tensor(cutlass.Int64, (sym(),), divisibility=1), # positions
|
||||
make_fake_tensor(cutlass.Int64, (sym(),), divisibility=1), # slots
|
||||
make_fake_tensor(bf16, (sym(), num_q_heads * _HEAD), divisibility=8),
|
||||
make_fake_tensor(bf16, (sym(),), divisibility=8), # k cache (flat)
|
||||
make_fake_tensor(bf16, (sym(),), divisibility=8), # v cache (flat)
|
||||
]
|
||||
kernel = _kernels()[1](bf16w, rope, num_q_heads, num_kv_heads)
|
||||
stream = cute.runtime.make_fake_stream(use_tvm_ffi_env_stream=True)
|
||||
return cute.compile(
|
||||
kernel, *args, Int32(1), Int32(1), Float32(1e-6), Int64(1), Int64(1),
|
||||
Int64(1), Int32(16), stream, options="--enable-tvm-ffi",
|
||||
) # fmt: skip
|
||||
|
||||
|
||||
def flint_qkv_fused(
|
||||
x: torch.Tensor,
|
||||
residual: torch.Tensor | None,
|
||||
norm_weight: torch.Tensor,
|
||||
eps: float,
|
||||
weight: torch.Tensor,
|
||||
weight_scale: torch.Tensor | None,
|
||||
global_scale: torch.Tensor | None,
|
||||
q_norm_weight: torch.Tensor,
|
||||
k_norm_weight: torch.Tensor,
|
||||
qk_eps: float,
|
||||
cos_sin_cache: torch.Tensor | None,
|
||||
positions: torch.Tensor,
|
||||
k_cache: torch.Tensor,
|
||||
v_cache: torch.Tensor,
|
||||
slot_mapping: torch.Tensor,
|
||||
num_q_heads: int,
|
||||
num_kv_heads: int,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Residual-add RMSNorm + qkv GEMV + q/k norm, RoPE, cache write for
|
||||
``M <= 8`` tokens.
|
||||
|
||||
Args:
|
||||
x: [M, 2048] bf16 attention input (contiguous).
|
||||
residual: [M, 2048] bf16 residual, or None for the first layer.
|
||||
norm_weight: Input RMSNorm weight.
|
||||
eps: Input RMSNorm epsilon.
|
||||
weight: qkv weight, NVFP4 (uint8 [N, 1024]) or BF16 [N, 2048].
|
||||
weight_scale: NVFP4 128x4-swizzled block scales (None for BF16).
|
||||
global_scale: NVFP4 fp32 global scale (None for BF16).
|
||||
q_norm_weight: Per-head q RMSNorm weight.
|
||||
k_norm_weight: Per-head k RMSNorm weight.
|
||||
qk_eps: q/k RMSNorm epsilon.
|
||||
cos_sin_cache: [max_pos, 128] fp32 (cos | sin), or None (no RoPE).
|
||||
positions: [M] int64.
|
||||
k_cache: [blocks, block_size, kv_heads, 128] view.
|
||||
v_cache: Same shape and strides as ``k_cache``.
|
||||
slot_mapping: [<= M] int64; negative slots and tokens beyond it are
|
||||
not written.
|
||||
num_q_heads: Number of q heads.
|
||||
num_kv_heads: Number of KV heads.
|
||||
|
||||
Returns:
|
||||
(q [M, num_q_heads * 128] bf16, residual_out [M, 2048] bf16).
|
||||
|
||||
"""
|
||||
m = x.shape[0]
|
||||
assert 0 < m <= MAX_M and x.shape[1] == _K and x.is_contiguous()
|
||||
bf16w = weight.dtype == torch.bfloat16
|
||||
n = (num_q_heads + 2 * num_kv_heads) * _HEAD
|
||||
assert weight.shape[0] == n and weight.is_contiguous()
|
||||
assert k_cache.stride() == v_cache.stride() and k_cache.stride(-1) == 1
|
||||
has_residual = residual is not None
|
||||
if residual is not None:
|
||||
assert residual.is_contiguous()
|
||||
res_out = torch.empty_like(x)
|
||||
else:
|
||||
residual = res_out = x
|
||||
xn = torch.empty_like(x)
|
||||
_compile_norm(has_residual)(x, residual, res_out, xn, norm_weight, m, eps)
|
||||
q = torch.empty(m, num_q_heads * _HEAD, dtype=x.dtype, device=x.device)
|
||||
w = weight.view(torch.uint8).view(-1)
|
||||
s = w if weight_scale is None else weight_scale.view(torch.uint8).view(-1)
|
||||
gs = global_scale.reshape(1) if global_scale is not None else _one(x.device)
|
||||
rope = cos_sin_cache is not None
|
||||
if cos_sin_cache is not None:
|
||||
assert cos_sin_cache.dtype == torch.float32 and cos_sin_cache.shape[1] == _HEAD
|
||||
cs = cos_sin_cache
|
||||
else:
|
||||
cs = no_rope_table(x.device)
|
||||
_compile_qkv(bf16w, rope, num_q_heads, num_kv_heads)(
|
||||
xn, w, s, gs, q_norm_weight, k_norm_weight, cs, positions, slot_mapping,
|
||||
q, _base(k_cache), _base(v_cache), m, slot_mapping.shape[0], qk_eps,
|
||||
k_cache.stride(0), k_cache.stride(1), k_cache.stride(2), k_cache.shape[1],
|
||||
) # fmt: skip
|
||||
return q, res_out
|
||||
|
||||
|
||||
def _base(t: torch.Tensor) -> torch.Tensor:
|
||||
return torch.as_strided(t, (_HEAD,), (1,))
|
||||
|
||||
|
||||
_NO_ROPE: dict[torch.device, torch.Tensor] = {}
|
||||
|
||||
|
||||
def no_rope_table(device: torch.device) -> torch.Tensor:
|
||||
"""Placeholder fp32 cos|sin argument for kernels compiled without RoPE."""
|
||||
t = _NO_ROPE.get(device)
|
||||
if t is None:
|
||||
t = _NO_ROPE[device] = torch.zeros(1, _HEAD, device=device)
|
||||
return t
|
||||
@@ -169,6 +169,7 @@ _CONFIG_REGISTRY: dict[str, type[PreTrainedConfig]] = LazyConfigDict(
|
||||
**{"unlimited-ocr": "UnlimitedOCRConfig"},
|
||||
unlimited_ocr="UnlimitedOCRConfig",
|
||||
**{"deepseek_v41": "DeepseekV41Config"},
|
||||
flint="FlintConfig",
|
||||
inkling_mm_model="InklingMMConfig",
|
||||
inkling_model="InklingModelConfig",
|
||||
)
|
||||
@@ -821,6 +822,10 @@ def maybe_override_with_speculators(
|
||||
Tuple of (resolved_model, resolved_tokenizer, speculative_config)
|
||||
|
||||
"""
|
||||
# A single local file (e.g. a .gguf checkpoint) has no speculators config.
|
||||
if os.path.isfile(model):
|
||||
return model, tokenizer, vllm_speculative_config
|
||||
|
||||
kwargs["local_files_only"] = huggingface_hub.constants.HF_HUB_OFFLINE
|
||||
config_dict, _ = PreTrainedConfig.get_config_dict(
|
||||
model,
|
||||
|
||||
@@ -74,6 +74,7 @@ _CLASS_TO_MODULE: dict[str, str] = {
|
||||
"Step5Config": "vllm.transformers_utils.configs.step5",
|
||||
"Step5VLConfig": "vllm.transformers_utils.configs.step5",
|
||||
"Qwen3ASRConfig": "vllm.transformers_utils.configs.qwen3_asr",
|
||||
"FlintConfig": "vllm.models.flint.configs",
|
||||
"InklingModelConfig": "vllm.models.inkling.configs",
|
||||
"InklingAudioConfig": "vllm.models.inkling.configs",
|
||||
"InklingVisionConfig": "vllm.models.inkling.configs",
|
||||
@@ -144,6 +145,7 @@ __all__ = [
|
||||
"Step5Config",
|
||||
"Step5VLConfig",
|
||||
"Qwen3ASRConfig",
|
||||
"FlintConfig",
|
||||
"InklingModelConfig",
|
||||
"InklingAudioConfig",
|
||||
"InklingVisionConfig",
|
||||
|
||||
Reference in new issue
Block a user