Skip to content

Commit cd91fec

Browse files
mudlerclaude
andcommitted
27B dense bring-up: W4A4 loader routing + dense SwiGLU forward (CPU)
Continues the 27B (Qwen3_5ForConditionalGeneration, text_config qwen3_5_text) bring-up — §5 steps 2-3 of qwen27b-w4a4-notes.md, CPU-only (no GPU touched). Loader (qwen3_5_dense.h / qwen3_5_dense_weights.cpp), mirroring LoadQwen3_5Moe: - Qwen3_5DenseWeights = embed + final_norm + bf16 lm_head + per-layer Qwen3_5DenseLayerWeights (reused GdnLayerWeights/FullAttnLayerWeights + new DenseMlpWeights gate/up/down). - IsQwen27QuantizedLinear routes each Linear bf16 vs W4A4 per the §3.6 ignore list; MaterializeCtNvfp4Bf16Transposed dequants W4A4 -> bf16 (Matmul-B) via the existing DequantCtNvfp4WeightToF32 (CT weight_packed/weight_scale/ weight_global_scale names). Activation-quant dropped on this bf16 path (step-6a fast path). Dense forward (qwen3_5.cpp), mirroring Qwen3_5Model::ForwardDense: - Qwen3_5DenseModel::ForwardDense reuses the 35B GdnBlock/FullAttnBlock/norm helpers verbatim; DenseMlpBlock = down(silu(gate(x))*up(x)) replaces MoeBlock. Text path only; ViT/merger/MTP deferred. CPU tests (test_qwen27_dense_forward.cpp, 4 cases / 280 assertions): loader routing, W4A4 materialization vs the hand-computed CT block, finite+deterministic forward, and MLP-perturbation-moves-output. Full CPU ctest suite 83/83 green; clean -Werror rebuild. test_qwen27_paged_engine stays SKIPPING (kW4A4ForwardReady false — flips at the GPU oracle+GEMM step). Notes §5 + porting-inventory §9.7 updated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UJyFKcK62CcR3imhgbiBnW
1 parent b870856 commit cd91fec

8 files changed

Lines changed: 758 additions & 22 deletions

File tree

.agents/porting-inventory.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,21 @@ Examples: `examples/cli` ✅ (C-API client), `examples/server` ✅ (OpenAI serve
275275
→ C ABI callback registries, added per tier.
276276
5. **GGUF as a first-class input** (upstream treats it as one loader among many;
277277
for us it is a gate, including NVFP4 GGUF extension types).
278-
6. **Extension platforms** (T2): Apple Metal and Vulkan backends — upstream has
278+
7. **Explicit owned-tensor model weights** (no `nn.Module`/`AutoWeightsLoader`).
279+
The dense 27B (`Qwen3_5ForConditionalGeneration`, text_config `qwen3_5_text`)
280+
loads through `LoadQwen3_5Dense` into `Qwen3_5DenseWeights` — mirroring the
281+
35B's `LoadQwen3_5Moe` — with the MoE block replaced by a `DenseMlpWeights`
282+
SwiGLU MLP. Two recorded remaps: (a) the compressed-tensors NVFP4 W4A4 tensor
283+
NAMES (`weight_packed`/`weight_scale`/`weight_global_scale`/`input_global_scale`)
284+
vs the 35B modelopt names (`weight`/`weight_scale`/`weight_scale_2`/`input_scale`);
285+
(b) on the CPU correctness path every W4A4 Linear is MATERIALIZED to bf16 at
286+
load (`MaterializeCtNvfp4Bf16Transposed``DequantCtNvfp4WeightToF32`) so the
287+
existing bf16 forward carries it — the fp4-resident tensor-core GEMM reuse is
288+
the later GPU step (qwen27b-w4a4-notes.md §5 steps 6-7), not a permanent
289+
deviation. Per-Linear bf16-vs-W4A4 routing is `IsQwen27QuantizedLinear`
290+
(encodes the checkpoint `ignore` list, §3.6). Text path only; the ViT/merger
291+
and MTP head are deferred stubs.
292+
8. **Extension platforms** (T2): Apple Metal and Vulkan backends — upstream has
279293
no equivalent under `vllm/platforms/`; we add them through the mirrored
280294
Platform/AttentionBackend/vt-op seams so they behave as vLLM platforms
281295
would. Intel is NOT a deviation (upstream `platforms/xpu.py` is ported

.agents/qwen27b-w4a4-notes.md

Lines changed: 45 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,20 @@ pinned upstream checkout `/home/mudler/_git/vllm` @ `e24d1b24`. Cites are
1010
Checkpoint (read-only on dgx):
1111
`~/.cache/huggingface/hub/models--unsloth--Qwen3.6-27B-NVFP4`.
1212

13-
STATUS (2026-07-04): CPU-first correctness scaffolding only. Delivered: the CPU
14-
W4A4 dequant + activation-quant reference (`nvfp4_emulation.h`, unit-tested) and
15-
the skipping greedy-parity gate scaffold (`test_qwen27_paged_engine.cpp`). The
16-
27B forward + the GB10 W4A4 GPU GEMM are NOT yet written — §5 is the ordered
17-
plan, GPU-gated steps marked.
13+
STATUS (2026-07-04): CPU-first correctness path LANDED (§5 steps 1-4). Delivered:
14+
the CPU W4A4 dequant + activation-quant reference (`nvfp4_emulation.h`,
15+
unit-tested); the dense loader (`LoadQwen3_5Dense``Qwen3_5DenseWeights`,
16+
`qwen3_5_dense.h`/`qwen3_5_dense_weights.cpp`) that routes each Linear bf16 vs
17+
W4A4-materialized-to-bf16 by name (`IsQwen27QuantizedLinear` +
18+
`MaterializeCtNvfp4Bf16Transposed`); the dense TEXT forward
19+
(`Qwen3_5DenseModel::ForwardDense` in `qwen3_5.cpp`, reusing the 35B GDN +
20+
gated-attn + norm helpers with the MoE block swapped for the dense SwiGLU MLP);
21+
and CPU unit tests (`test_qwen27_dense_forward.cpp`: routing + materialization +
22+
finite/deterministic forward + MLP-wired, 4 cases / 280 assertions, CPU-green).
23+
The full CPU ctest suite (83 targets) stays green. What is NOT done and is GPU-gated: the
24+
pip-vLLM oracle greedy golden capture (step 5), the W4A4 matmul kernel wiring
25+
(step 6), and flipping `kW4A4ForwardReady` to close the gate (step 7). §5 is the
26+
ordered plan, GPU steps marked.
1827

1928
---
2029

@@ -250,22 +259,37 @@ Legend: **[CPU]** doable on the dev box now; **[GPU]** needs the free GB10.
250259
the pinned emulation math (`test_ct_nvfp4_emulation.cpp`, 6 cases / 81
251260
assertions, CPU-green). This is the CPU-truth the GPU GEMM validates against.
252261

253-
2. **[CPU] Config + loader plumbing.** Recognize
254-
`Qwen3_5ForConditionalGeneration` (dense; `is_moe=false`), read `text_config`
255-
(§1), the compressed-tensors quant block + the `ignore` list, and
256-
`partial_rotary_factor`/`mrope_section`/`output_gate_type`/`attn_output_gate`
257-
from `HfConfig.raw`. Add a dense-MLP + W4A4 weight-loader path (mirror the
258-
existing modelopt loader in `qwen3_5_weights.cpp`): route bf16 vs W4A4 by name
259-
(§3.6); for the CPU reference forward, materialize each W4A4 linear to bf16
260-
via `DequantCtNvfp4WeightToF32` (as the 35B loader dequants modelopt).
261-
Mirror upstream names so the port stays mechanical; record the tensor-name
262-
remap in porting-inventory §9.
263-
264-
3. **[CPU] Dense forward assembly.** New `Qwen3_5Model::ForwardDense`-style path
265-
for the dense arch: reuse the 35B GDN + full-attn + norm components, swap the
266-
MoE block for the dense SwiGLU MLP (§2), wire the quantized linears. Validate
267-
per-layer activations + greedy on CPU where feasible (full 64-layer CPU
268-
greedy is slow — the authoritative gate is GPU, step 6).
262+
2. **[CPU] ✅ DONE — Config + loader plumbing.** `LoadHfConfig` already
263+
recognizes `Qwen3_5ForConditionalGeneration` via the `text_config`/
264+
`qwen3_5_text` resolution (partial_rotary_factor default 0.25 → rotary_dim 64;
265+
`num_experts` reads 0 = dense). `LoadQwen3_5Dense`
266+
(`qwen3_5_dense.h`/`qwen3_5_dense_weights.cpp`) mirrors `LoadQwen3_5Moe`:
267+
`Qwen3_5DenseWeights` = embed + final_norm + bf16 `lm_head` + per-layer
268+
`Qwen3_5DenseLayerWeights` (reused `GdnLayerWeights`/`FullAttnLayerWeights` +
269+
new `DenseMlpWeights`). Each Linear is routed bf16 vs W4A4-materialized-to-bf16
270+
by `IsQwen27QuantizedLinear` (encodes the §3.6 `ignore` list) and materialized
271+
via `MaterializeCtNvfp4Bf16Transposed``DequantCtNvfp4WeightToF32` (reads the
272+
CT `weight_packed`/`weight_scale`/`weight_global_scale` names; the on-disk
273+
`input_global_scale` activation divisor is IGNORED on this bf16-activation path
274+
— the step-6a fast path). Tensor-name remap recorded in porting-inventory §9.7.
275+
276+
3. **[CPU] ✅ DONE — Dense forward assembly.** `Qwen3_5DenseModel::ForwardDense`
277+
(in `qwen3_5.cpp`, so it reuses the file-local `GdnBlock`/`FullAttnBlock`/norm/
278+
`DBuf`/matmul helpers verbatim) mirrors `Qwen3_5Model::ForwardDense`: embed →
279+
N `RunDenseLayer` (input_layernorm → GDN|full-attn → post_attn_layernorm →
280+
`DenseMlpBlock`) → final RMSNorm → bf16 `lm_head`. `DenseMlpBlock` =
281+
`down( silu(gate(x)) * up(x) )` (the shared-expert silu-mul, no router/gate).
282+
Text-only: the three mRoPE streams coincide so partial NeoX RoPE degenerates to
283+
1-D over `positions` (§2). CPU-validated on a synthetic small hybrid model
284+
(finite + deterministic logits; MLP-perturbation moves the output). The ViT +
285+
image/video merger + MTP head are DEFERRED (text-first, §0.1) — no stub code,
286+
the loader simply does not request `model.visual.*`/`mtp.*`.
287+
⚠ REMAINING before step 5/7 flips the gate: (a) a paged `Qwen3_5DenseModel`
288+
forward + a runner route (the 35B rides `Qwen3_5Model::Forward` + the runner;
289+
the dense arch needs the analogous paged path OR the runner extended to the
290+
dense weights) — the dense ForwardDense here is the single-sequence reference,
291+
as the 35B's ForwardDense was before its paged refactor; (b) mrope_section
292+
handling for genuine multimodal positions (inert for text).
269293

270294
4. **[CPU] ✅ DONE — greedy-parity gate scaffold.**
271295
`test_qwen27_paged_engine.cpp` resolves the `unsloth/Qwen3.6-27B-NVFP4`

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ add_library(vllm STATIC
6767
src/vllm/model_executor/model_loader/nvfp4_dequant.cpp
6868
src/vllm/model_executor/layers/quantization/compressed_tensors/nvfp4_emulation.cpp
6969
src/vllm/model_executor/models/qwen3_5_weights.cpp
70+
src/vllm/model_executor/models/qwen3_5_dense_weights.cpp
7071
src/vllm/model_executor/models/qwen3_5_gguf_weights.cpp
7172
src/vllm/model_executor/models/qwen3_5.cpp
7273
src/vllm/model_executor/models/registry.cpp
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
// vllm.cpp original — the DENSE Qwen3.6-27B text gate
2+
// (unsloth/Qwen3.6-27B-NVFP4, arch Qwen3_5ForConditionalGeneration, text_config
3+
// model_type "qwen3_5_text"). See .agents/qwen27b-w4a4-notes.md.
4+
//
5+
// The 27B shares the 35B hybrid backbone WHOLESALE (GDN linear-attention + gated
6+
// full-attention + Gemma (1+w) RMSNorm + mRoPE->NeoX); it REUSES the 35B's
7+
// GdnLayerWeights / FullAttnLayerWeights sub-structs and the GdnBlock /
8+
// FullAttnBlock forward helpers verbatim. The ONLY structural change is the
9+
// per-layer sparse-MoE block being replaced by a DENSE SwiGLU MLP (gate/up/down,
10+
// intermediate 17408): down( silu(gate(x)) * up(x) ). See notes §2.
11+
//
12+
// Quant: compressed-tensors NVFP4 W4A4 (notes §3). For the CPU correctness path
13+
// each quantized Linear is MATERIALIZED to bf16 at load via the CT weight-dequant
14+
// reference (DequantCtNvfp4WeightToF32, multiply by 1/weight_global_scale) and
15+
// the existing bf16 forward carries it — the true GB10 fp4xfp4 GEMM is a later,
16+
// GPU-gated step (notes §5 steps 5-7). The activation-quant round-trip is dropped
17+
// on this correctness path (bf16 activations), matching the notes' §5 step-6a
18+
// FAST PATH; that is a tiny numeric deviation vs true W4A4, validated later vs
19+
// the pip-vLLM oracle golden.
20+
//
21+
// Which Linears are quantized (notes §3.6): QUANTIZED (W4A4) = every dense-MLP
22+
// gate/up/down_proj, every self_attn q/k/v/o_proj, and the GDN linear_attn
23+
// out_proj. NOT quantized (bf16 on disk) = the GDN in_proj_{qkv,z,a,b}, conv1d,
24+
// A_log, dt_bias, all norms, embed_tokens, lm_head, mtp.*, and visual.*.
25+
#pragma once
26+
27+
#include <cstdint>
28+
#include <string>
29+
#include <vector>
30+
31+
#include "vllm/model_executor/models/qwen3_5_weights.h" // OwnedTensor, Gdn/FullAttn weights, TensorResolver
32+
#include "vllm/transformers_utils/hf_config.h"
33+
#include "vt/device.h"
34+
#include "vt/tensor.h"
35+
36+
namespace vllm {
37+
38+
// Dense SwiGLU MLP (replaces the 35B MoE block). Projections in Matmul-B layout
39+
// [in, out]; W4A4-materialized to bf16 at load. down( silu(gate(x)) * up(x) ).
40+
struct DenseMlpWeights {
41+
OwnedTensor gate_proj; // bf16 [H, I]
42+
OwnedTensor up_proj; // bf16 [H, I]
43+
OwnedTensor down_proj; // bf16 [I, H]
44+
};
45+
46+
// One dense decoder layer: input/post norms + one attention variant + dense MLP.
47+
struct Qwen3_5DenseLayerWeights {
48+
bool is_linear_attention = false;
49+
OwnedTensor input_layernorm; // bf16 [H]
50+
OwnedTensor post_attention_layernorm; // bf16 [H]
51+
GdnLayerWeights gdn; // valid iff is_linear_attention
52+
FullAttnLayerWeights attn; // valid iff !is_linear_attention
53+
DenseMlpWeights mlp; // every layer has a dense MLP
54+
};
55+
56+
// Whole dense-model text weights. lm_head is bf16 (unquantized in the 27B).
57+
struct Qwen3_5DenseWeights {
58+
OwnedTensor embed_tokens; // bf16 [vocab, H] (NOT transposed; embed lookup)
59+
OwnedTensor final_norm; // bf16 [H]
60+
OwnedTensor lm_head; // bf16 [H, vocab] (unquantized -> Matmul-B layout)
61+
std::vector<Qwen3_5DenseLayerWeights> layers;
62+
};
63+
64+
// True iff the projection named `name` is a W4A4-quantized Linear in the 27B
65+
// (notes §3.6). `name` is the module path WITHOUT the trailing ".weight*" (e.g.
66+
// "model.language_model.layers.0.mlp.gate_proj"). Encodes the checkpoint's
67+
// config.json `ignore` list: the quantized set is the dense-MLP {gate,up,down}
68+
// proj, the self_attn {q,k,v,o} proj, and the GDN linear_attn out_proj; every
69+
// other Linear (GDN in_proj_*, lm_head, mtp.*, visual.*, ...) is bf16.
70+
bool IsQwen27QuantizedLinear(const std::string& name);
71+
72+
// Materialize one compressed-tensors NVFP4 W4A4 Linear to an owned bf16 tensor
73+
// in Matmul-B layout [in, out]. Reads `<proj>.weight_packed` (U8 [out, in/2]),
74+
// `<proj>.weight_scale` (F8_E4M3 [out, in/16]) and `<proj>.weight_global_scale`
75+
// (F32 scalar divisor); dequants to f32 via DequantCtNvfp4WeightToF32 (which
76+
// reciprocates the global scale), rounds to bf16, and transposes. Exposed for
77+
// unit testing. The `<proj>.input_global_scale` (activation divisor) is ignored
78+
// on this bf16-activation correctness path (notes §3.4 / §5 step-6a).
79+
OwnedTensor MaterializeCtNvfp4Bf16Transposed(const TensorResolver& get,
80+
const std::string& proj);
81+
82+
// Load one dense decoder layer. `layer_type` is "linear_attention" or
83+
// "full_attention". Prefix is "model.language_model.layers.{layer_idx}.". Routes
84+
// each Linear to bf16 vs W4A4-materialized-to-bf16 per IsQwen27QuantizedLinear.
85+
Qwen3_5DenseLayerWeights LoadQwen3_5DenseLayer(const TensorResolver& get,
86+
const std::string& layer_type,
87+
int64_t layer_idx);
88+
89+
// Full dense-model load across the given shards. Uses config.num_hidden_layers
90+
// and config.layer_types. Text path only — the vision tower (model.visual.*),
91+
// the MTP head (mtp.*) and the image/video merger are DEFERRED (notes §0.1).
92+
Qwen3_5DenseWeights LoadQwen3_5Dense(const std::vector<SafetensorsFile>& shards,
93+
const HfConfig& config);
94+
95+
// Dense single-sequence reference forward (text path). Mirrors
96+
// Qwen3_5Model::ForwardDense but runs the dense SwiGLU MLP in place of the MoE
97+
// block; reuses the 35B GDN + gated-attention + norm forward helpers. Returns
98+
// logits [T, vocab] f32 (T = token_ids.size()). CPU or CUDA per `queue`.
99+
class Qwen3_5DenseModel {
100+
public:
101+
static std::vector<float> ForwardDense(const std::vector<int32_t>& token_ids,
102+
const std::vector<int32_t>& positions,
103+
const Qwen3_5DenseWeights& weights,
104+
const HfConfig& config,
105+
vt::Queue& queue);
106+
};
107+
108+
} // namespace vllm

src/vllm/model_executor/models/qwen3_5.cpp

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
// .agents/moe-semantics.md (§1-§6 MoE block + activated-expert gather).
88
#include "vllm/model_executor/models/qwen3_5.h"
99

10+
#include "vllm/model_executor/models/qwen3_5_dense.h"
11+
1012
#include <algorithm>
1113
#include <cmath>
1214
#include <cstdlib>
@@ -1089,6 +1091,46 @@ void RunLayer(Dev d, const Qwen3_5MoeLayerWeights& layer, const HfConfig& cfg,
10891091
hidden = MoeBlock(d, layer.moe, cfg, dh2.t(), T);
10901092
}
10911093

1094+
// --- Dense SwiGLU MLP block (the 27B's replacement for the MoE block; notes
1095+
// §2). down( silu(gate(x)) * up(x) ), intermediate = cfg.intermediate_size.
1096+
// Mirrors the shared-expert silu-mul MLP (no router, no expert gather, no output
1097+
// gate). h [T,H] bf16 (device) -> DBuf [T,H] bf16 (device). Reused by the dense
1098+
// forward below; the gate/up/down weights are W4A4-materialized-to-bf16 at load.
1099+
DBuf DenseMlpBlock(Dev d, const DenseMlpWeights& w, const HfConfig& cfg,
1100+
const Tensor& dh, int64_t T) {
1101+
const int64_t I = cfg.intermediate_size;
1102+
DBuf gate = MatmulF32D(d, dh, w.gate_proj); // [T,I] f32
1103+
DBuf up = MatmulF32D(d, dh, w.up_proj); // [T,I] f32
1104+
DBuf act(d, DType::kBF16, {T, I});
1105+
vt::MoeSiluMul(d.q, act.t(), gate.t(), up.t()); // silu(gate)*up -> bf16
1106+
return MatmulBf16D(d, act.t(), w.down_proj); // [T,H] bf16
1107+
}
1108+
1109+
// One dense decoder layer (notes §2). Same residual/norm thread as RunLayer, but
1110+
// the MoE block is swapped for the dense SwiGLU MLP; the GDN / full-attention
1111+
// blocks are the 35B helpers reused verbatim. `hidden` (bf16 [T,H]) is the delta;
1112+
// `res` (f32 [T,H]) the accumulator.
1113+
void RunDenseLayer(Dev d, const Qwen3_5DenseLayerWeights& layer,
1114+
const HfConfig& cfg, DBuf& hidden, DBuf& res,
1115+
const std::vector<int32_t>& positions, int64_t T) {
1116+
const int64_t H = cfg.hidden_size;
1117+
const float eps = static_cast<float>(cfg.rms_norm_eps);
1118+
1119+
Tensor dw_in = ResidentWeight(d, layer.input_layernorm, {H});
1120+
DBuf dhn(d, DType::kBF16, {T, H});
1121+
vt::RmsNorm(d.q, dhn.t(), hidden.t(), dw_in, vt::RmsNormArgs{eps, true}, &res.t());
1122+
1123+
DBuf attn = layer.is_linear_attention
1124+
? GdnBlock(d, layer.gdn, cfg, dhn.t(), T)
1125+
: FullAttnBlock(d, layer.attn, cfg, dhn.t(), positions, T);
1126+
1127+
Tensor dw_post = ResidentWeight(d, layer.post_attention_layernorm, {H});
1128+
DBuf dh2(d, DType::kBF16, {T, H});
1129+
vt::RmsNorm(d.q, dh2.t(), attn.t(), dw_post, vt::RmsNormArgs{eps, true}, &res.t());
1130+
1131+
hidden = DenseMlpBlock(d, layer.mlp, cfg, dh2.t(), T);
1132+
}
1133+
10921134
// Batched PAGED decoder layer (M1.8 Task 3). Same residual/norm/MoE thread as
10931135
// RunLayer, but the attention block reads/writes the paged KV cache
10941136
// (full-attn: attn_kv) or the persistent GDN mamba state (GDN: gdn_state).
@@ -1299,6 +1341,49 @@ std::vector<float> Qwen3_5Model::ForwardDense(const std::vector<int32_t>& token_
12991341
return logits;
13001342
}
13011343

1344+
std::vector<float> Qwen3_5DenseModel::ForwardDense(
1345+
const std::vector<int32_t>& token_ids, const std::vector<int32_t>& positions,
1346+
const Qwen3_5DenseWeights& weights, const HfConfig& config,
1347+
vt::Queue& queue) {
1348+
const int64_t T = static_cast<int64_t>(token_ids.size());
1349+
const int64_t H = config.hidden_size;
1350+
const int64_t vocab = config.vocab_size;
1351+
VT_CHECK(T > 0, "qwen3_5 dense forward: empty token_ids");
1352+
VT_CHECK(static_cast<int64_t>(positions.size()) == T,
1353+
"qwen3_5 dense forward: positions length must equal token count");
1354+
VT_CHECK(static_cast<int64_t>(weights.layers.size()) == config.num_hidden_layers,
1355+
"qwen3_5 dense forward: weights.layers size must equal num_hidden_layers");
1356+
Dev d{vt::GetBackend(queue.device.type), queue};
1357+
const float eps = static_cast<float>(config.rms_norm_eps);
1358+
1359+
// Embed: hidden = embed_tokens[token_ids] (bf16, device-resident). res = 0.
1360+
// For a TEXT-only step the three mRoPE position streams are identical, so the
1361+
// partial NeoX RoPE in FullAttnBlock degenerates to 1-D RoPE over `positions`
1362+
// (notes §2). The vision tower / image-video merger are DEFERRED.
1363+
Tensor dtab = ResidentWeight(d, weights.embed_tokens, {vocab, H});
1364+
DBuf dids(d, DType::kI32, {T}, token_ids.data());
1365+
DBuf hidden(d, DType::kBF16, {T, H});
1366+
vt::Embedding(d.q, hidden.t(), dtab, dids.t());
1367+
1368+
DBuf res(d, DType::kF32, {T, H});
1369+
res.Zero(d);
1370+
1371+
for (int64_t l = 0; l < config.num_hidden_layers; ++l)
1372+
RunDenseLayer(d, weights.layers[static_cast<size_t>(l)], config, hidden, res,
1373+
positions, T);
1374+
1375+
// Final RMSNorm over the fused stream (res += hidden; norm), then lm_head.
1376+
Tensor dfn = ResidentWeight(d, weights.final_norm, {H});
1377+
DBuf dnorm(d, DType::kBF16, {T, H});
1378+
vt::RmsNorm(d.q, dnorm.t(), hidden.t(), dfn, vt::RmsNormArgs{eps, true}, &res.t());
1379+
1380+
// lm_head is unquantized bf16 in the 27B (notes §3.6): the one host Download.
1381+
DBuf dlogits = MatmulF32D(d, dnorm.t(), weights.lm_head);
1382+
std::vector<float> logits(static_cast<size_t>(T) * vocab);
1383+
dlogits.Download(d, logits.data());
1384+
return logits;
1385+
}
1386+
13021387
std::vector<float> Qwen3_5ReplayLayer(const Qwen3_5MoeLayerWeights& layer,
13031388
const HfConfig& config,
13041389
const std::vector<float>& hidden_in,

0 commit comments

Comments
 (0)