-
Notifications
You must be signed in to change notification settings - Fork 32
Expand file tree
/
Copy pathqwen3_5.cpp
More file actions
9616 lines (9124 loc) · 490 KB
/
Copy pathqwen3_5.cpp
File metadata and controls
9616 lines (9124 loc) · 490 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// vllm.cpp original; see qwen3_5.h. Forward math mirrored 1:1 from the pinned
// upstream (qwen3_next.py::Qwen3NextDecoderLayer / Qwen3NextModel.forward,
// qwen_gdn_linear_attn.py, qwen3_next.py::Qwen3NextAttention /
// Qwen3NextSparseMoeBlock @ e24d1b24). References:
// .agents/specs/qwen36-forward-notes.md (assembly, §2 mRoPE->NeoX, §5 attention),
// .agents/specs/gdn-semantics.md (§1 layout, §6 g/beta prep, §7 recurrence),
// .agents/specs/moe-semantics.md (§1-§6 MoE block + activated-expert gather).
#include "vllm/model_executor/models/qwen3_5.h"
#include "vllm/model_executor/models/decode_graph_sizes.h"
#include "vllm/model_executor/models/device_pool.h" // DevicePool/Pool/AuxPool/ActivePool (shared)
#include "vllm/model_executor/models/qwen3_5_dense.h"
#include "vllm/model_executor/models/qwen3_5_internal.h"
#include "vllm/model_executor/models/qwen3_5_moe_block.h" // RunMoeBlock (SEAM GAP #2 exposure)
#include "vllm/model_executor/models/qwen3_5_mtp.h"
#include "vllm/model_executor/models/qwen3_vl_text.h" // M3-b: Qwen3VLGetRopeIndex (MRoPE positions)
#include "vllm/platforms/interface.h" // GetPlatform(device.type) per-tensor residency seam
#include <algorithm>
#include <array>
#include <atomic>
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <map>
#include <memory>
#include <mutex>
#include <string>
#include <unordered_map>
#include <utility>
#include <optional>
#include <vector>
#include "vllm/model_executor/models/dense_nvfp4_gemm.h" // dense_nvfp4::MarlinDenseEnabled
#include "vllm/model_executor/model_loader/nvfp4_dequant.h"
#include "vt/backend.h"
#ifdef VT_BENCH_PROFILE_CONTROL
#include "vt/cuda/cuda_profiler_control.h"
#endif
#include "vt/dtype.h"
#include "vt/ops.h"
#include "vt/recipes.h"
#ifdef VT_MARLIN_NVFP4
#include "vt/cuda/marlin_repack.h"
#endif
namespace vllm {
namespace {
// Telemetry: how many times the MIXED spec+non-spec GDN batch path
// (GdnBlockPagedMixedSpec) ran this process. Incremented per GDN layer per
// mixed step; a nonzero count proves the concurrency split/merge was actually
// exercised (a c>1 spec run that never mixed would leave it at 0). Used by the
// c>1 identity gate to distinguish "mixed batch handled" from "pure-spec only".
std::atomic<int64_t> g_mixed_spec_invocations{0};
} // namespace
int64_t Qwen3_5MixedSpecInvocations() {
return g_mixed_spec_invocations.load(std::memory_order_relaxed);
}
void ResetQwen3_5MixedSpecInvocations() {
g_mixed_spec_invocations.store(0, std::memory_order_relaxed);
}
bool detail::ShouldUsePackedGdnDecode(
const GdnPackedDecodeEligibility& e) {
return e.runtime_enabled && e.cuda && e.dense_model && e.has_packed_ba &&
e.merged_ba_enabled && e.dtype_compatible && e.has_state_indices &&
e.num_prefills == 0 && e.num_prefill_tokens == 0 &&
e.num_spec_decodes == 0 && e.num_spec_decode_tokens == 0 &&
e.num_decodes > 0 && e.num_decode_tokens == e.num_decodes &&
e.num_actual_tokens == e.num_decode_tokens;
}
// PERF-27B-GDN-PACKED-REACHABLE (#365). Mirrors ProjectGdnQkvz's branch order:
// the merged BF16 owner wins, then the native-FP8 owner, then the split BF16
// owner. Order matters — a checkpoint that carries both owners projects through
// the BF16 one, so predicting the FP8 dtype there would be wrong.
vt::DType detail::GdnProjectedMixedQkvDType(const GdnMixedQkvDTypeInputs& in) {
if (in.has_bf16_qkvz_owner) return in.in_dtype;
// PERF-GDN-PACKED-BRIDGE (#365): only the MERGED fp8 arm can carry the
// narrowed epilogue dtype; the split arm hardcodes F32 (ProjectGdnQkvz).
if (in.has_fp8_qkv_owner)
return in.fp8_merged_arm ? in.fp8_out_dtype : vt::DType::kF32;
return in.in_dtype;
}
// PERF-GDN-PACKED-BRIDGE (#365). PERF-FP8-ALPHA-FOLD's three terms, verbatim,
// in the ONE place both the producer and the predictor read. See the header for
// why each term is required; the short version is that the toggle is the opt-in,
// `indt` keeps VT_GDN_IN_BF16's rollback honest on this arm too, and `outdt`
// confines the narrowing to the dense 27B.
vt::DType detail::GdnFp8MergedMixedQkvDType(bool fp8_in_bf16_enabled,
vt::DType in_dtype,
vt::DType out_dtype) {
return (fp8_in_bf16_enabled && in_dtype == vt::DType::kBF16 &&
out_dtype == vt::DType::kBF16)
? vt::DType::kBF16
: vt::DType::kF32;
}
// The dtype rule vt::GdnPackedDecode actually imposes (ops.cpp gdn_packed_decode
// dtype checks), plus the model leg's BF16 pin. No term keys on weight storage.
bool detail::GdnPackedDecodeDTypesCompatible(const GdnPackedDecodeDTypes& d) {
// Uniformity over the four activation tensors, pinned to BF16.
if (d.mixed_qkv != vt::DType::kBF16) return false;
if (d.ba_out != d.mixed_qkv || d.core_out != d.mixed_qkv) return false;
// The recurrent state is independent: any of the dtypes the caches can hold.
return d.ssm_state == vt::DType::kF32 || d.ssm_state == vt::DType::kF16 ||
d.ssm_state == vt::DType::kBF16;
}
// Default OFF: ON only for a '1'-leading value (vt::cuda::GdnPackedRegTileFlagIsOn).
bool detail::PackedGdnDecodeFp8TowerFlagIsOn(const char* env_value) {
return env_value != nullptr && env_value[0] == '1';
}
bool detail::ShouldUseMergedGdnQkvz(const GdnMergedQkvzEligibility& e) {
return e.runtime_enabled && e.cuda && e.has_packed_qkvz && e.uniform_dtype;
}
// PERF-27B-GDN-FP8-QKVZ. Every term is required: dropping any one of them must
// leave the exact two legacy fp8 GEMMs. `shared_input_scale` is the load-time
// scale-compatibility guard (the merged GEMM quantizes the activation ONCE, so
// the two shards must agree bitwise on the per-tensor activation scale) and is
// the term that keeps a checkpoint whose scales differ on the split path.
bool detail::ShouldUseMergedGdnFp8Qkvz(const GdnMergedFp8QkvzEligibility& e) {
return e.runtime_enabled && e.fp8_platform && e.has_fp8_shards &&
e.shared_k && e.shared_input_scale && e.shard_widths_match;
}
// True (and *scale filled) only when both fp8 GDN input shards are populated and
// carry the SAME per-tensor activation scale, by exact float equality. This is
// the single definition; `Fp8SharedInputScale`'s linear-attention branch calls
// it, so the fused RmsNorm+quant guard and the merge guard cannot drift.
bool detail::GdnFp8SharedInputScale(const GdnLayerWeights& gdn, float* scale) {
if (gdn.in_proj_qkv_fp8.Empty() || gdn.in_proj_z_fp8.Empty()) return false;
if (gdn.in_proj_qkv_fp8.input_scale != gdn.in_proj_z_fp8.input_scale)
return false;
if (scale != nullptr) *scale = gdn.in_proj_qkv_fp8.input_scale;
return true;
}
bool detail::MergedGdnFp8QkvzEnvSelected(const GdnMergedFp8QkvzEnvConfig& env) {
if (env.merged_proj != nullptr && env.merged_proj[0] == '0') return false;
if (env.merged_qkvz != nullptr && env.merged_qkvz[0] == '0') return false;
return env.merged_qkvz_fp8 == nullptr || env.merged_qkvz_fp8[0] != '0';
}
namespace {
std::atomic<bool> g_gdn_fp8_inproj_debug_enabled{false};
std::atomic<uint64_t> g_gdn_fp8_inproj_merged{0};
std::atomic<uint64_t> g_gdn_fp8_inproj_split{0};
} // namespace
void detail::ResetGdnFp8InProjDebugStats() {
g_gdn_fp8_inproj_merged.store(0, std::memory_order_relaxed);
g_gdn_fp8_inproj_split.store(0, std::memory_order_relaxed);
g_gdn_fp8_inproj_debug_enabled.store(true, std::memory_order_release);
}
detail::GdnFp8InProjDebugStats detail::GetGdnFp8InProjDebugStats() {
GdnFp8InProjDebugStats out;
out.merged_launches = g_gdn_fp8_inproj_merged.load(std::memory_order_relaxed);
out.split_launches = g_gdn_fp8_inproj_split.load(std::memory_order_relaxed);
return out;
}
void detail::DisableGdnFp8InProjDebugStats() {
g_gdn_fp8_inproj_debug_enabled.store(false, std::memory_order_release);
}
bool detail::PackedGdnDecodeEnvSelected(const GdnPackedDecodeEnvConfig& env) {
// Mirror PackedGdnDecodeRuntimeEnabled: enabled unless first char is '0'.
const bool runtime_enabled =
env.packed_decode == nullptr || env.packed_decode[0] != '0';
// Mirror MergedGdnBaEnabled's env core: master '0' wins; leaf default-on.
const bool merged_ba =
!(env.merged_proj != nullptr && env.merged_proj[0] == '0') &&
(env.merged_ba == nullptr || env.merged_ba[0] != '0');
// Mirror the dtype_compatible expression on the real 27B dense gate:
// GdnInDType (default BF16), GdnOutDType dense default (BF16; override
// '0' -> F32), MergedGdnBaOutputDType(packed) (default BF16 under packed;
// override '0' -> F32). The SSM cache dtype term is always a float dtype.
const bool in_bf16 = env.in_bf16 == nullptr || env.in_bf16[0] != '0';
const bool out_bf16 = env.out_bf16 == nullptr || env.out_bf16[0] != '0';
const bool ba_out_bf16 =
env.ba_out_bf16 == nullptr || env.ba_out_bf16[0] != '0';
return runtime_enabled && merged_ba && in_bf16 && out_bf16 && ba_out_bf16;
}
// VT_GDN_VALIDATE=1 (read ONCE) forces the exhaustive O(n^2) pairwise
// duplicate cross-verification on top of the default O(n) seen-set pass. The
// default already fails closed on duplicate/out-of-range/negative live slots;
// this env is a redundant paranoid verifier for debugging, never required for
// correctness.
static bool GdnForceFullValidationEnv() {
static const bool on = [] {
const char* e = std::getenv("VT_GDN_VALIDATE");
return e != nullptr && e[0] == '1' && e[1] == '\0';
}();
return on;
}
void detail::ValidateGdnStateIndices(const std::vector<int32_t>& indices,
int64_t required,
int64_t state_slots,
bool force_full_uniqueness) {
VT_CHECK(required >= 0 &&
required <= static_cast<int64_t>(indices.size()),
"qwen3_5: GDN state index metadata is too short");
VT_CHECK(state_slots >= 0,
"qwen3_5: GDN state cache has invalid slot count");
// O(n) uniqueness: a live slot is in [0, state_slots) and drawn from a
// free-list of distinct slots by construction, so a single seen-set pass
// fails closed on any duplicate/out-of-range/negative slot without the former
// O(n^2) inner scan. `seen` is bounded by state_slots (== max_num_reqs,
// small) and lives only for this call; -1 is the inert padding sentinel.
std::vector<uint8_t> seen(static_cast<size_t>(state_slots), 0);
for (int64_t i = 0; i < required; ++i) {
const int32_t slot = indices[static_cast<size_t>(i)];
if (slot < 0) {
VT_CHECK(slot == -1,
"qwen3_5: invalid negative GDN state index");
continue;
}
VT_CHECK(slot < state_slots,
"qwen3_5: GDN state index out of range");
VT_CHECK(seen[static_cast<size_t>(slot)] == 0,
"qwen3_5: duplicate live GDN state index");
seen[static_cast<size_t>(slot)] = 1;
}
// Paranoid exhaustive re-verification (VT_GDN_VALIDATE=1 or an explicit
// caller request). Identical verdict to the seen-set pass above.
if (force_full_uniqueness || GdnForceFullValidationEnv()) {
for (int64_t i = 0; i < required; ++i) {
const int32_t slot = indices[static_cast<size_t>(i)];
if (slot < 0) continue;
for (int64_t j = 0; j < i; ++j) {
VT_CHECK(indices[static_cast<size_t>(j)] != slot,
"qwen3_5: duplicate live GDN state index");
}
}
}
}
void detail::ValidateGdnAttentionMetadata(
const v1::GDNAttentionMetadata& metadata, int64_t state_slots,
bool allow_inert_padding) {
const int64_t nd = metadata.num_decodes;
const int64_t np = metadata.num_prefills;
const int64_t nd_tok = metadata.num_decode_tokens;
const int64_t np_tok = metadata.num_prefill_tokens;
// Spec-decode segmentation (SPEC-MTP I5a). ns == 0 on every production step —
// the default path validated below is byte-identical to pre-I5a.
const int64_t ns = metadata.num_spec_decodes;
const int64_t ns_tok = metadata.num_spec_decode_tokens;
const int64_t nreq = nd + np;
VT_CHECK(nd >= 0 && np >= 0 && nd_tok >= 0 && np_tok >= 0 &&
ns >= 0 && ns_tok >= 0 && metadata.num_actual_tokens >= 0,
"qwen3_5: negative GDN metadata count");
VT_CHECK(nd_tok == nd,
"qwen3_5: non-spec decode requires one token per request");
VT_CHECK(metadata.num_actual_tokens == nd_tok + np_tok + ns_tok,
"qwen3_5: GDN decode+prefill+spec tokens must equal actual tokens");
// ── Spec metadata validation (SPEC-MTP I5a; mirrors the gdn_attn.py build()
// spec contract, src/vllm/v1/attention/backends/gdn_attn.cpp:181-276). Runs
// only when the step actually carries drafts; on the default path (ns == 0)
// none of it executes. ──
if (ns > 0) {
const int64_t num_cols = metadata.spec_state_indices_num_cols;
VT_CHECK(num_cols >= 1,
"qwen3_5: spec GDN state slot column count must be >= 1");
VT_CHECK(metadata.spec_state_indices_tensor.has_value() &&
metadata.spec_query_start_loc.has_value() &&
metadata.spec_sequence_masks.has_value() &&
metadata.spec_token_indx.has_value() &&
metadata.num_accepted_tokens.has_value(),
"qwen3_5: incomplete spec GDN metadata");
const std::vector<int32_t>& ssi = *metadata.spec_state_indices_tensor;
const std::vector<int32_t>& sqsl = *metadata.spec_query_start_loc;
const std::vector<int32_t>& nat = *metadata.num_accepted_tokens;
const std::vector<int32_t>& stx = *metadata.spec_token_indx;
VT_CHECK(static_cast<int64_t>(ssi.size()) == ns * num_cols,
"qwen3_5: spec_state_indices_tensor must be [num_spec_decodes, "
"num_spec+1]");
VT_CHECK(static_cast<int64_t>(sqsl.size()) == ns + 1,
"qwen3_5: spec_query_start_loc must be [num_spec_decodes + 1]");
VT_CHECK(static_cast<int64_t>(nat.size()) == ns,
"qwen3_5: num_accepted_tokens must be [num_spec_decodes]");
VT_CHECK(static_cast<int64_t>(stx.size()) == ns_tok,
"qwen3_5: spec_token_indx must be [num_spec_decode_tokens]");
VT_CHECK(sqsl.front() == 0 && sqsl.back() == ns_tok,
"qwen3_5: spec query offsets must span the spec tokens");
for (int64_t i = 0; i < ns; ++i) {
VT_CHECK(sqsl[static_cast<size_t>(i + 1)] > sqsl[static_cast<size_t>(i)],
"qwen3_5: spec query offsets must be strictly increasing");
const int32_t acc = nat[static_cast<size_t>(i)];
VT_CHECK(acc >= 1 && acc <= num_cols,
"qwen3_5: num_accepted_tokens must be in [1, num_spec + 1]");
// The INITIAL-state slot (column num_accepted-1) must be a live slot in
// range; per-timestep snapshot slots may be null (< 0) only under padding.
const int32_t init_slot =
ssi[static_cast<size_t>(i * num_cols + (acc - 1))];
VT_CHECK(init_slot >= 0 || allow_inert_padding,
"qwen3_5: spec initial GDN state slot must be live");
VT_CHECK(init_slot < state_slots,
"qwen3_5: spec GDN state slot out of range");
}
for (int32_t slot : ssi)
VT_CHECK(slot < state_slots,
"qwen3_5: spec GDN state slot out of range");
for (int32_t t : stx)
VT_CHECK(t >= 0 && t < metadata.num_actual_tokens,
"qwen3_5: spec_token_indx entry out of range");
if (metadata.non_spec_token_indx.has_value()) {
VT_CHECK(static_cast<int64_t>(metadata.non_spec_token_indx->size()) ==
nd_tok + np_tok,
"qwen3_5: non_spec_token_indx must cover the non-spec tokens");
}
}
if (nreq == 0 && ns == 0) {
VT_CHECK(metadata.num_actual_tokens == 0,
"qwen3_5: GDN tokens require state metadata");
return;
}
// Pure spec batch (no non-spec rows): the non-spec segmentation is nullopt by
// construction (gdn_attn.cpp:202-217), so skip the non-spec validation below.
if (nreq == 0) return;
VT_CHECK(metadata.non_spec_state_indices_tensor.has_value(),
"qwen3_5: missing non-spec GDN state indices");
const std::vector<int32_t>& indices =
*metadata.non_spec_state_indices_tensor;
VT_CHECK(static_cast<int64_t>(indices.size()) == nreq,
"qwen3_5: non-spec GDN state index count must equal request count");
detail::ValidateGdnStateIndices(indices, nreq, state_slots);
if (!allow_inert_padding) {
for (int32_t slot : indices) {
VT_CHECK(slot >= 0,
"qwen3_5: live GDN state index must be non-negative");
}
}
if (np == 0) return;
VT_CHECK(metadata.non_spec_query_start_loc.has_value() &&
metadata.has_initial_state.has_value() &&
metadata.prefill_state_indices.has_value() &&
metadata.prefill_query_start_loc.has_value() &&
metadata.prefill_has_initial_state.has_value(),
"qwen3_5: incomplete GDN prefill metadata");
const std::vector<int32_t>& full_qsl =
*metadata.non_spec_query_start_loc;
const std::vector<uint8_t>& full_initial = *metadata.has_initial_state;
const std::vector<int32_t>& prefill_indices =
*metadata.prefill_state_indices;
const std::vector<int32_t>& prefill_qsl =
*metadata.prefill_query_start_loc;
const std::vector<uint8_t>& prefill_initial =
*metadata.prefill_has_initial_state;
VT_CHECK(static_cast<int64_t>(full_qsl.size()) == nreq + 1 &&
static_cast<int64_t>(full_initial.size()) == nreq,
"qwen3_5: non-spec GDN prefill metadata has invalid shape");
VT_CHECK(static_cast<int64_t>(prefill_indices.size()) == np &&
static_cast<int64_t>(prefill_qsl.size()) == np + 1 &&
static_cast<int64_t>(prefill_initial.size()) == np,
"qwen3_5: prefill-only GDN metadata has invalid shape");
// The non-spec cu_seqlens spans the NON-SPEC tokens only. On the default path
// (ns == 0) that equals num_actual_tokens; in a MIXED spec batch the spec
// tokens (ns_tok) are carried by the separate spec segmentation, so the
// non-spec span is nd_tok + np_tok < num_actual_tokens (gdn_attn.cpp:240-253).
VT_CHECK(full_qsl.front() == 0 && full_qsl.back() == nd_tok + np_tok,
"qwen3_5: non-spec GDN query offsets must span the non-spec tokens");
for (int64_t i = 0; i < nreq; ++i) {
VT_CHECK(full_qsl[static_cast<size_t>(i + 1)] >
full_qsl[static_cast<size_t>(i)],
"qwen3_5: non-spec GDN query offsets must be strictly increasing");
}
VT_CHECK(full_qsl[static_cast<size_t>(nd)] == nd_tok,
"qwen3_5: non-spec GDN decode prefix does not match token count");
for (int64_t i = 0; i < np; ++i) {
const size_t prefill_row = static_cast<size_t>(i);
const size_t full_row = static_cast<size_t>(nd + i);
VT_CHECK(prefill_indices[prefill_row] == indices[full_row],
"qwen3_5: prefill state indices must match non-spec suffix");
VT_CHECK(prefill_indices[prefill_row] >= 0,
"qwen3_5: prefill GDN state index must be non-negative");
VT_CHECK(prefill_qsl[prefill_row] ==
full_qsl[full_row] - nd_tok,
"qwen3_5: prefill query offsets must match rebased non-spec suffix");
VT_CHECK(prefill_initial[prefill_row] == full_initial[full_row],
"qwen3_5: prefill initial-state mask must match non-spec suffix");
}
VT_CHECK(prefill_qsl.back() == np_tok,
"qwen3_5: prefill query offsets must span prefill tokens");
VT_CHECK(metadata.batch_ptr.has_value() &&
metadata.token_chunk_offset_ptr.has_value(),
"qwen3_5: missing exact causal-conv chunk metadata");
const v1::CausalConv1dMetadata expected_conv =
v1::ComputeCausalConv1dMetadata(full_qsl);
VT_CHECK(*metadata.batch_ptr == expected_conv.batch_ptr &&
*metadata.token_chunk_offset_ptr ==
expected_conv.token_chunk_offset_ptr,
"qwen3_5: causal-conv chunk metadata does not exactly cover query offsets");
}
bool detail::CanUseGdnDecodeGraphSize(int64_t real_batch,
int64_t capture_batch,
bool indexed_state_io) {
return real_batch > 0 && capture_batch >= real_batch &&
(capture_batch == real_batch || indexed_state_io);
}
int64_t detail::ValidateGdnStateCacheLayout(
const std::vector<GdnStateCache>& state_caches) {
if (state_caches.empty()) return 0;
int64_t state_slots = -1;
for (const GdnStateCache& cache : state_caches) {
VT_CHECK(cache.ssm_state.rank == 4 && cache.conv_state.rank == 3,
"qwen3_5: GDN SSM/conv state ranks must be 4/3");
VT_CHECK(cache.ssm_state.shape[0] == cache.conv_state.shape[0],
"qwen3_5: GDN conv/SSM state slot counts must match");
if (state_slots < 0) {
state_slots = cache.ssm_state.shape[0];
} else {
VT_CHECK(cache.ssm_state.shape[0] == state_slots,
"qwen3_5: all GDN layers must use the same state slot count");
}
}
return state_slots;
}
// ENG-ASYNC-SCHED W4 (see qwen3_5_internal.h for why this is a scoped override
// rather than a parameter on five entry points). Thread-local: one host thread
// drives a forward, and a serving process may drive independent engines from
// different threads, so a process-global would let one engine's device ids leak
// into another's embed.
detail::DeviceTokenIds& detail::DeviceTokenIdsOverride() {
thread_local DeviceTokenIds ids;
return ids;
}
vt::DType detail::ResolveMambaSsmCacheDType(const HfConfig& config,
vt::DType conv_dtype) {
const std::string& dtype = config.mamba_ssm_dtype;
if (dtype.empty() || dtype == "auto") return conv_dtype;
if (dtype == "float32" || dtype == "float") return vt::DType::kF32;
if (dtype == "float16" || dtype == "half") return vt::DType::kF16;
if (dtype == "bfloat16") return vt::DType::kBF16;
throw std::runtime_error(
"qwen3_5: unsupported mamba_ssm_dtype '" + dtype +
"' (expected float16/half, bfloat16, float32/float, or auto)");
}
void detail::ValidateGdnDecodeGraphState(
const v1::GDNAttentionMetadata& metadata,
const std::vector<GdnStateCache>& state_caches, int64_t real_batch) {
VT_CHECK(real_batch > 0,
"qwen3_5 decode graph: real batch must be positive");
// SPEC-DSPARK W8 (#442): a UNIFORM SPEC batch is capturable too. vLLM's
// captured decode length is `1 + num_speculative_tokens`
// (cudagraph_dispatcher.py:37), so its T=1+k verify is graphed by construction
// while ours was refused right here. The assertion is RE-EXPRESSED for that
// shape, never dropped: a spec batch is still required to be EXACT and pure
// (no prefill mixed in), with every token a spec token.
const bool spec_batch = metadata.num_spec_decodes > 0;
if (spec_batch) {
VT_CHECK(metadata.num_prefills == 0 && metadata.num_prefill_tokens == 0 &&
metadata.num_decodes == 0 && metadata.num_decode_tokens == 0 &&
metadata.num_spec_decode_tokens == real_batch &&
metadata.num_actual_tokens == real_batch,
"qwen3_5 decode graph: a spec batch must be an exact PURE spec "
"decode (every token a spec token, no prefill)");
// Uniformity is what makes the shape a graph key: every request contributes
// the same 1+k query span, so real_batch == num_spec_decodes * (1+k).
VT_CHECK(metadata.num_spec_decodes > 0 &&
real_batch % metadata.num_spec_decodes == 0,
"qwen3_5 decode graph: spec batch must be uniform across requests");
VT_CHECK(metadata.spec_state_indices_tensor.has_value(),
"qwen3_5 decode graph: missing GDN spec state indices");
VT_CHECK(!state_caches.empty(),
"qwen3_5 decode graph: missing GDN state caches");
return;
}
VT_CHECK(metadata.num_prefills == 0 && metadata.num_prefill_tokens == 0 &&
metadata.num_spec_decodes == 0 &&
metadata.num_spec_decode_tokens == 0 &&
metadata.num_decodes == real_batch &&
metadata.num_decode_tokens == real_batch &&
metadata.num_actual_tokens == real_batch,
"qwen3_5 decode graph: metadata must describe exact pure non-spec "
"decode");
VT_CHECK(metadata.non_spec_state_indices_tensor.has_value(),
"qwen3_5 decode graph: missing GDN state indices");
const std::vector<int32_t>& indices =
*metadata.non_spec_state_indices_tensor;
VT_CHECK(static_cast<int64_t>(indices.size()) == real_batch,
"qwen3_5 decode graph: state index count must equal the real decode "
"batch");
VT_CHECK(!state_caches.empty(),
"qwen3_5 decode graph: missing GDN state caches");
const int64_t state_slots =
detail::ValidateGdnStateCacheLayout(state_caches);
for (int64_t i = 0; i < real_batch; ++i) {
VT_CHECK(indices[static_cast<size_t>(i)] >= 0,
"qwen3_5 decode graph: live GDN state index must be non-negative");
}
ValidateGdnStateIndices(indices, real_batch, state_slots);
}
DenseGateUpGlobals MergeDenseGateUpGlobals(const Nvfp4Weight& gate,
const Nvfp4Weight& up) {
VT_CHECK(gate.weight_global_scale_inv > 0.0F &&
up.weight_global_scale_inv > 0.0F,
"qwen3_5 dense merged gate_up: missing CT weight divisor");
VT_CHECK(gate.input_global_scale_inv > 0.0F &&
up.input_global_scale_inv > 0.0F,
"qwen3_5 dense merged gate_up: missing CT input divisor");
DenseGateUpGlobals globals;
globals.input_global_scale_inv =
std::max(gate.input_global_scale_inv, up.input_global_scale_inv);
const float weight_global_scale_inv =
std::max(gate.weight_global_scale_inv, up.weight_global_scale_inv);
// Preserve vLLM/PyTorch's operation order: reciprocal each selected maximum,
// then multiply. Do not derive the maximum divisor back from scale2.
const float input_global_scale = 1.0F / globals.input_global_scale_inv;
globals.weight_global_scale = 1.0F / weight_global_scale_inv;
globals.alpha = input_global_scale * globals.weight_global_scale;
return globals;
}
FullAttnQkvGlobals MergeFullAttnQkvGlobals(const Nvfp4Weight& q,
const Nvfp4Weight& k,
const Nvfp4Weight& v) {
VT_CHECK(q.weight_global_scale_inv > 0.0F &&
k.weight_global_scale_inv > 0.0F &&
v.weight_global_scale_inv > 0.0F,
"qwen3_5 packed QKV: missing CT weight divisor");
VT_CHECK(q.input_global_scale_inv > 0.0F &&
k.input_global_scale_inv > 0.0F &&
v.input_global_scale_inv > 0.0F,
"qwen3_5 packed QKV: missing CT input divisor");
FullAttnQkvGlobals globals;
globals.input_global_scale_inv =
std::max({q.input_global_scale_inv, k.input_global_scale_inv,
v.input_global_scale_inv});
const float weight_global_scale_inv =
std::max({q.weight_global_scale_inv, k.weight_global_scale_inv,
v.weight_global_scale_inv});
const float input_global_scale = 1.0F / globals.input_global_scale_inv;
globals.weight_global_scale = 1.0F / weight_global_scale_inv;
globals.alpha = input_global_scale * globals.weight_global_scale;
return globals;
}
namespace {
using vt::Backend;
using vt::Device;
using vt::DType;
using vt::Queue;
using vt::Tensor;
using v1::CommonAttentionMetadata;
using v1::GDNAttentionMetadata;
// Backend + queue bundle threaded through every helper.
struct Dev {
Backend& b;
Queue& q;
};
Tensor MakeTensor(void* data, DType dt, vt::Device dev,
const std::vector<int64_t>& shape) {
Tensor t;
t.data = data;
t.dtype = dt;
t.device = dev;
t.rank = static_cast<int>(shape.size());
int64_t acc = 1;
for (int i = t.rank - 1; i >= 0; --i) {
t.shape[i] = shape[static_cast<size_t>(i)];
t.stride[i] = acc;
acc *= t.shape[i];
}
return t;
}
// Contiguous reinterpret of a device tensor's buffer at a new shape (same numel,
// same dtype/device). Used to view [T,H,D] as [T*H,D] etc. for rank-2 ops.
Tensor Reshape(const Tensor& src, const std::vector<int64_t>& shape) {
return MakeTensor(src.data, src.dtype, src.device, shape);
}
// DevicePool / Pool() / AuxPool() / ActivePool() / ActivePoolScope now live in
// the shared header include/vllm/model_executor/models/device_pool.h (extracted
// VERBATIM so the dense Qwen3 forward reuses the same pooled scratch; behavior
// here is byte-for-byte unchanged).
// The device-scratch residency policy (BACKEND-PLATFORM item 2), resolved from
// the running device's platform (per-object: keyed on the DBuf's own
// device.type, NOT the process-global CurrentPlatform). The DevicePool soft cap
// is now platform data, not an inline constant — a discrete GPU sets a bound and
// this file is unchanged. Memoized PER DEVICE TYPE because DBuf is a per-op hot
// path; platforms are fixed at static registration, so the value never changes
// afterward. It used to be ONE function-local static, which cached whichever
// device asked first and applied that cap to every later one — the same
// ambient-device assumption #516 fixed one layer down (dense_device_glue.h
// carries the identical repair). A backend whose platform was never REGISTERED
// therefore throws out of GetPlatform rather than inheriting the first device's
// cap — a cap read off another platform is a wrong number, not a default.
struct DevicePoolPolicy {
size_t cap_bytes = 0; // residency_policy().device_pool_cap_bytes (0 == uncapped)
};
DevicePoolPolicy ResolveDevicePoolPolicy(const Dev& d) {
// cap+1, so 0 means "not resolved yet" and a genuine cap of 0 (every platform
// today) still caches. Racing threads resolve the same type to the same value.
static std::array<std::atomic<size_t>, vt::kNumDeviceTypes> cached{};
// Same bound platforms::Index() applies to this identical value before
// indexing ITS registry (src/vllm/platforms/platform.cpp).
const size_t idx = static_cast<size_t>(d.q.device.type);
VT_CHECK(idx < vt::kNumDeviceTypes, "invalid device type");
const size_t seen = cached[idx].load(std::memory_order_relaxed);
if (seen != 0) return DevicePoolPolicy{seen - 1};
const auto rp = vllm::platforms::GetPlatform(d.q.device.type).residency_policy();
cached[idx].store(rp.device_pool_cap_bytes + 1, std::memory_order_relaxed);
return DevicePoolPolicy{rp.device_pool_cap_bytes};
}
// --- Fused-MoE per-layer resident constants (M2.5 Phase 2, CUDA-graph unblock) -
// MoeBlockFusedCuda used to rebuild + re-upload, EVERY forward step, a set of
// per-layer CONSTANT device buffers: the E fp4-expert device-pointer/scale
// arrays (gate/up/down packed+scale ptrs, scale2) and the pair->token row map
// (tok_map, a function of T only). Those uploads copy from HOST STACK temporaries
// — illegal to have inside a CUDA-graph capture region (their host addresses
// dangle on replay). They are also pure per-step waste (the values never change).
// This process-lifetime cache (keyed by the layer's MoeBlockWeights address)
// uploads them ONCE, during the pre-warm forward, so the captured region only
// READS resident device buffers — no host-sourced copy, nothing to dangle. The
// device buffers leak at process exit (like the cublasLt workspace / the resident
// weights); they are bounded by (num_layers * (9*E + one tok_map per distinct T)).
struct MoeFusedResident {
void* gp = nullptr; // i64 [E] device: expert gate packed ptrs
void* gs = nullptr; // i64 [E] device: expert gate scale ptrs
void* up = nullptr; // i64 [E] device: expert up packed ptrs
void* us = nullptr; // i64 [E] device: expert up scale ptrs
void* dp = nullptr; // i64 [E] device: expert down packed ptrs
void* ds = nullptr; // i64 [E] device: expert down scale ptrs
void* g2 = nullptr; // f32 [E] device: expert gate scale2
void* u2 = nullptr; // f32 [E] device: expert up scale2
void* d2 = nullptr; // f32 [E] device: expert down scale2
std::unordered_map<int64_t, void*> tok_map; // T -> i32 [T*top_k] device
bool ready = false;
};
// Fetch (building on first use) the resident state a weight owns. Replaces the
// process-lifetime `static std::unordered_map<const W*, R>` these accessors used
// to be: keying on the weight's ADDRESS let a second engine inherit a freed
// engine's device pointers (issue #237). See ResidentSlot in qwen3_5_weights.h.
//
// The lock is the one the map accessors already took on every call, kept rather
// than narrowed: this fix is about lifetime, and quietly changing the
// synchronisation of a hot path at the same time would make any regression
// ambiguous between the two.
template <typename R>
R& ResidentIn(const ResidentSlot& slot) {
static std::mutex mu;
std::lock_guard<std::mutex> lk(mu);
if (!slot.state) slot.state = std::make_shared<R>();
return *static_cast<R*>(slot.state.get());
}
MoeFusedResident& MoeResidentFor(const MoeBlockWeights* w) {
return ResidentIn<MoeFusedResident>(w->resident_fused);
}
// --- BF16 fast-MoE per-layer resident constants (Qwen3-Coder Qwen3MoeForCausalLM,
// W5). The bf16 analog of MoeFusedResident: the E per-expert bf16 [K,N] weight
// DEVICE pointers (gate/up/down) + the pair->token row map, uploaded ONCE during
// the pre-warm forward so the captured decode region only reads resident device
// buffers (no host-sourced copy to dangle on graph replay). The device pointers
// are the stable ResidentWeight uploads (each OwnedTensor's d_dev owns the copy
// for process lifetime); we capture them once. Leaked at process exit like the
// fp4 resident arrays / cublasLt workspace.
struct MoeBf16Resident {
void* gate = nullptr; // i64 [E] device: per-expert gate weight ptrs ([H,I] bf16)
void* up = nullptr; // i64 [E] device: per-expert up weight ptrs ([H,I] bf16)
void* down = nullptr; // i64 [E] device: per-expert down weight ptrs ([I,H] bf16)
std::unordered_map<int64_t, void*> tok_map; // T -> i32 [T*top_k] device
bool ready = false;
};
MoeBf16Resident& MoeBf16ResidentFor(const MoeBlockWeights* w) {
return ResidentIn<MoeBf16Resident>(w->resident_bf16);
}
// Fast BF16 grouped-MoE path (Qwen3-Coder). DEFAULT ON per the parity-enablers-
// ship-as-defaults policy: the lever the every-axis speed parity depends on ships
// default-ON, gated, BEFORE the binding speed run. VT_MOE_BF16_FAST=0 restores the
// per-expert host-gather reference loop (the correctness oracle) for same-binary
// A/B. Only consulted for bf16 experts on CUDA (fp4 experts always take the fused
// Marlin/wmma path; CPU/GGUF keeps the reference loop regardless).
bool MoeBf16FastEnabled() {
static const bool on = [] {
const char* e = std::getenv("VT_MOE_BF16_FAST");
return !(e != nullptr && e[0] == '0'); // default ON; =0 rolls back
}();
return on;
}
// LAYOUT PRECONDITION for the fast bf16 grouped-MoE path. The grouped kernel reads
// each expert weight as a bf16 [K,N] Matmul-B buffer (element (k,n) at k*N+n) and
// the router gate through the plain `vt::Matmul` (B = [H,E]) — i.e. the
// `LoadBf16Transposed` orientation (`nk == false`) produced by the Qwen3-Coder
// safetensors loader (qwen3_moe_weights.cpp:77-85) and the GGUF loader
// (qwen3_5_gguf_weights.cpp:403-422, `LoadExpertsT` transposes to [in,out]).
// It CANNOT read the raw torch-Linear orientation (`nk == true`, [N,K]), which the
// 35B MTP loader produces (`LoadBf16RawNK`/`CopyRawNK`, qwen3_5_mtp.cpp:109-133) —
// the reference loop below handles that via MatmulF32/MatmulBf16's `w.nk` branch
// (qwen3_5.cpp:721-737). So the fast path is taken ONLY when every weight it reads
// is in the layout it supports; anything else falls through to the reference loop
// (correct on every layout). This keeps the new path provably inert for the 35B
// (fp4 experts), the MTP module (nk=true), and any future nk=true producer instead
// of silently transposing their math.
bool MoeBf16FastLayoutOk(const MoeBlockWeights& w, const HfConfig& cfg) {
const int64_t H = cfg.hidden_size, I = cfg.moe_intermediate_size;
const int64_t E = cfg.num_experts;
if (w.router_gate.nk || w.router_gate.rank != 2 || w.router_gate.shape[0] != H ||
w.router_gate.shape[1] != cfg.num_experts)
return false;
if (w.expert_gate.size() != static_cast<size_t>(E) ||
w.expert_up.size() != static_cast<size_t>(E) ||
w.expert_down.size() != static_cast<size_t>(E))
return false;
auto ok = [](const OwnedTensor& t, int64_t k, int64_t n) {
return !t.nk && t.rank == 2 && t.shape[0] == k && t.shape[1] == n &&
t.dtype == vt::DType::kBF16;
};
for (int64_t e = 0; e < E; ++e) {
const size_t se = static_cast<size_t>(e);
if (!ok(w.expert_gate[se], H, I) || !ok(w.expert_up[se], H, I) ||
!ok(w.expert_down[se], I, H))
return false;
}
return true;
}
#ifdef VT_MARLIN_NVFP4
// --- Marlin NVFP4 W4A16 MoE: per-layer resident repacked weights (M0.8 drop-in).
// When VT_NVFP4_MARLIN=1, the routed experts are repacked ONCE at first touch
// into Marlin's interleaved layout (gptq_marlin_moe_repack + S0E5M3 scales +
// processed global scales, all bit-exact to vLLM — tools/marlin/repack_*), and
// the wmma-fp4 fused path is replaced by moe_wna16_marlin_gemm. The original
// per-expert fp4 device copies are freed after repack (the wmma path is unused
// when the gate is on) so peak weight memory stays flat. Buffers leak at exit
// (like the wmma resident / cublasLt workspace).
struct MoeMarlinResident {
void* w_gate = nullptr; // i32 [E, K/16, N*2] (gate: size_k=K, size_n=N)
void* w_up = nullptr; // i32 [E, K/16, N*2]
void* w_down = nullptr; // i32 [E, N/16, K*2] (down: size_k=N, size_n=K)
void* s_gate = nullptr; // fp8 [E, K/16, N]
void* s_up = nullptr;
void* s_down = nullptr; // fp8 [E, N/16, K]
void* g_gate = nullptr; // f32 [E]
void* g_up = nullptr;
void* g_down = nullptr;
// Fused w13 layout (VT_MOE_FUSED_W13): gate+up CONCATENATED along N per expert
// — rows [0,N) = gate (vLLM w1), rows [N,2N) = up (vLLM w3) — repacked as ONE
// Marlin B operand of size_n=2N, mirroring vLLM's stacked w13_weight
// (marlin_utils_fp4.py prepare_nvfp4_moe_layer_for_marlin:374-401 repacks the
// stacked [E, 2N, K/2] per expert with size_n = num_shards*N). Populated
// INSTEAD of w_gate/w_up (same total bytes) when fused_w13 is true.
void* w_gu = nullptr; // i32 [E, K/16, (2N)*2]
void* s_gu = nullptr; // fp8 [E, K/16, 2N]
void* g_gu = nullptr; // f32 [E] (gate scale2 — vLLM w13_weight_scale_2[:, 0])
bool fused_w13 = false;
void* workspace = nullptr; // i32 [sms]
int sms = 0;
bool ready = false;
};
MoeMarlinResident& MoeMarlinResidentFor(const MoeBlockWeights* w) {
return ResidentIn<MoeMarlinResident>(w->resident_marlin);
}
bool MarlinMoeEnabled() {
// Default ON: the vendored Marlin NVFP4 W4A16 GEMM is the validated 35B path
// (measured gate +22%, decode-heavy +80%, 16/16 token-for-token vs the pinned
// oracle — see parity-ledger). Only an explicit VT_NVFP4_MARLIN=0 opts back out
// to the naive redundant-dequant / cublas bf16 GEMM (kept as an escape hatch).
static const bool on = [] {
const char* e = std::getenv("VT_NVFP4_MARLIN");
return !(e != nullptr && e[0] == '0');
}();
return on;
}
// Fused w13 grouped GEMM (VT_MOE_FUSED_W13, DEFAULT ON): run the routed
// experts' gate+up as ONE Marlin grouped GEMM over the N-concatenated w13
// weights (size_n=2I, output [P,2I]) + one SiluAndMul over the halves, instead
// of TWO grouped GEMMs (+2 workspace memsets, 2 schedule passes) + MoeSiluMul;
// same fusion for the shared-expert gate_up (SharedGateUpFusedMarlinD). This is
// exactly vLLM's marlin_moe.py shape: ONE moe_wna16_marlin_gemm with
// size_n = w13_num_shards * N into intermediate_cache1 [M*topk, 2N]
// (fused_moe/experts/marlin_moe.py:133-160), then silu_and_mul on the [:N]/[N:]
// halves (:162-170). At the 35B decode shape (I=512, top_k=8, many tiny
// latency-bound tiles) the second GEMM's fixed costs are pure overhead.
// GATED ON (GB10, 2026-07-10): fused-vs-split BIT-EXACT (test_ops_moe_grouped
// probe), 35B greedy 16/16-vs-oracle BOTH arms, and a clean same-binary A/B win
// (in1024/out128 np200 conc-64, 3+3 interleaved reps: 3166.85 -> 3262.28 tok/s
// = +3.01%, TPOT -3.1%, TTFT -1.4%; MoE-expert fusion alone +0.53%, the rest
// from the shared-expert gate_up fusion). VT_MOE_FUSED_W13=0 opts back out to
// the split two-GEMM layout for A/B.
// The layout choice is made at LOAD (BuildMoeMarlinResident builds either the
// fused or the split resident), so A/B = two runs of the same binary.
bool MoeFusedW13Enabled() {
static const bool on = [] {
const char* e = std::getenv("VT_MOE_FUSED_W13");
return !(e != nullptr && e[0] == '0');
}();
return on;
}
#endif // VT_MARLIN_NVFP4
// Owned device allocation + tensor view. On CPU the backend's Alloc/Copy are
// malloc/memcpy; on CUDA they are cudaMalloc / h2d-d2h on the queue's stream.
// Allocation is routed through the DevicePool so the buffer's storage is reused
// rather than freed to the driver (avoiding the cudaMalloc/cudaFree sync).
class DBuf {
public:
DBuf(Dev d, DType dt, const std::vector<int64_t>& shape,
const void* host = nullptr)
: b_(&d.b) {
int64_t numel = 1;
for (int64_t s : shape) numel *= s;
bytes_ = static_cast<size_t>(numel) * vt::SizeOf(dt);
alloc_bytes_ = bytes_ == 0 ? 1 : bytes_;
// Device-scratch soft cap comes from the platform residency policy
// (BACKEND-PLATFORM item 2), not an inline constant. 0 == uncapped (GB10
// today) ⇒ pool behavior is byte-for-byte unchanged.
cap_ = ResolveDevicePoolPolicy(d).cap_bytes;
// Draw from THIS DEVICE's pool (Pool(b)) unless an ActivePoolScope overrides
// it for the shared-expert overlap region (AuxPool(b)), and REMEMBER the
// pool so the block returns to the one it came from even when this DBuf
// outlives the scope (the aux region returns sd/gl, destroyed after the
// join). See AuxPool().
pool_ = &ActivePool(*b_);
p_ = pool_->Get(*b_, alloc_bytes_);
t_ = MakeTensor(p_, dt, d.q.device, shape);
if (host != nullptr) b_->Copy(d.q, p_, host, bytes_);
}
~DBuf() { if (p_ != nullptr) pool_->Put(*b_, alloc_bytes_, p_, cap_); }
DBuf(const DBuf&) = delete;
DBuf& operator=(const DBuf&) = delete;
// Movable so device-resident block helpers can RETURN a DBuf (the buffer
// ownership transfers; the moved-from buffer is not returned to the pool).
DBuf(DBuf&& o) noexcept
: b_(o.b_), pool_(o.pool_), p_(o.p_), bytes_(o.bytes_),
alloc_bytes_(o.alloc_bytes_), cap_(o.cap_), t_(o.t_) {
o.p_ = nullptr;
}
DBuf& operator=(DBuf&& o) noexcept {
if (this != &o) {
if (p_ != nullptr) pool_->Put(*b_, alloc_bytes_, p_, cap_);
b_ = o.b_;
pool_ = o.pool_;
p_ = o.p_;
bytes_ = o.bytes_;
alloc_bytes_ = o.alloc_bytes_;
cap_ = o.cap_;
t_ = o.t_;
o.p_ = nullptr;
}
return *this;
}
Tensor& t() { return t_; }
const Tensor& t() const { return t_; }
void* ptr() { return p_; }
size_t bytes() const { return bytes_; }
size_t alloc_bytes() const { return alloc_bytes_; }
// Relinquish ownership of the pool block WITHOUT returning it (the dtor becomes
// a no-op). The caller takes over the Put obligation for `alloc_bytes()`.
// The Tensor view (t()) still holds the raw data pointer after this. Prefer
// ReleaseShared(), which discharges that obligation correctly by construction.
void* Release() {
void* p = p_;
p_ = nullptr;
return p;
}
// Move the block into a shared_ptr that returns it to THIS buffer's own pool
// and backend. Replaces the hand-written deleter that closed over the byte
// count alone and called `Pool().Put(alloc, q)`, naming neither the device nor
// the pool — so it returned another device's block, and an aux-stream block,
// to the main device's free list (#516; see dense_device_glue.h).
std::shared_ptr<void> ReleaseShared() {
DevicePool* const pool = pool_;
Backend* const b = b_;
const size_t alloc = alloc_bytes_;
void* const p = Release();
if (p == nullptr) return {};
return std::shared_ptr<void>(p, [pool, b, alloc](void* q) { pool->Put(*b, alloc, q); });
}
void Zero(Dev d) { b_->Memset(d.q, p_, 0, bytes_); }
// Copies the buffer back to host and blocks until the queue is idle.
void Download(Dev d, void* host) {
b_->Copy(d.q, host, p_, bytes_);
b_->Synchronize(d.q);
}
private:
Backend* b_;
DevicePool* pool_ = nullptr; // owning scratch pool (this device's Pool() or AuxPool())
void* p_ = nullptr;
size_t bytes_ = 0;
size_t alloc_bytes_ = 0;
size_t cap_ = 0; // device-pool soft cap from residency_policy() (0 == uncapped)
Tensor t_;
};
float SizeF(int64_t n) { return static_cast<float>(n); }
float Silu(float x) { return x / (1.0F + std::exp(-x)); }
// Upcast a bf16 owned weight to an f32 host buffer (lossless). The CUDA norm /
// conv kernels require the weight dtype to match the activation dtype; where
// activations are f32 (GDN conv/gated-norm, attention qk-norm, final-norm
// replay), the bf16 weight must be presented as f32.
std::vector<float> WeightF32(const OwnedTensor& w) {
const auto* src = reinterpret_cast<const uint16_t*>(w.bytes.data());
const int64_t n = w.Numel();
std::vector<float> out(static_cast<size_t>(n));
for (int64_t i = 0; i < n; ++i) out[static_cast<size_t>(i)] = vt::BF16ToF32(src[i]);
return out;
}
// Device-resident raw-dtype view over an owned weight, uploaded ONCE (lazily)
// and reused across every forward step (mirrors ResidentNvfp4). The forward's
// bf16/f32 weights (embed table, layernorms, attention/GDN projections, router)
// were re-uploaded per op — the ~600MB embed table alone re-copied every step —
// dominating the measured 67.5%-of-wall cudaMemcpyAsync. Caching kills the
// re-upload: the shared_ptr in the (const) weight owns the device buffer for the
// model's lifetime. On CPU the bytes are already host-resident, so a direct view
// avoids the copy. The weight is a read-only matmul-B / norm / embed operand, so
// the const_cast is safe. `shape` defaults to the owned shape.
Tensor ResidentWeight(Dev d, const OwnedTensor& w, std::vector<int64_t> shape = {}) {
if (shape.empty()) shape.assign(w.shape, w.shape + w.rank);
// HOST-POINTER ALIASING IS A CPU PROPERTY, NOT A "NOT-CUDA" PROPERTY (issue
// #125). This read `!needs_weight_staging()`, which is true ONLY on CUDA
// (platforms/cuda.cpp; the base default is false and neither Vulkan, Metal nor
// XPU overrides it) -- so every DEVICE backend except CUDA aliased the host
// weight bytes into a tensor tagged with a device and handed a HOST pointer to
// a DEVICE kernel. On Vulkan that surfaces as "embedding: table points outside
// every Vulkan allocation" on the first native kernel of the forward.
//
// The correct predicate is `is_cpu()`: alias when the "device" IS the host,
// upload otherwise. It leaves CPU and CUDA on exactly the branches they already
// took (CPU: is_cpu true / staging false -> alias; CUDA: is_cpu false / staging
// true -> upload), so this cannot change either.
//
// include/vllm/model_executor/models/dense_attn_block.h carries the SAME helper
// already fixed this way, and 25 model files inherit it. This file is not one of
// them -- it kept a private copy, so the fix never reached it. That is the
// off-framework-model hazard the decode-framework-routing audit names.
if (vllm::platforms::GetPlatform(d.q.device.type).is_cpu()) {
Tensor t = MakeTensor(const_cast<uint8_t*>(w.bytes.data()), w.dtype,
d.q.device, shape);
// CIQ G7: carry the i8mm-repack marker from the OwnedTensor to the vt::Tensor
// the GEMM actually sees. This is the ONLY host->kernel weight-tensor
// construction on the CPU forward (MakeTensor drops it by default), so
// without this the kernel reads repacked bytes as a plain q8_0 weight ->
// garbage. Only ever true on the CPU keep-quant path (a staged device never
// repacks), so it is inert everywhere else.
t.repacked = w.repacked;
// Same reasoning for the elementwise [N,K] -> [K,N] repack: without this the
// kernel would read transposed bytes as a plain [N,K] weight. Set only on
// this CPU-resident construction, which is exactly where MatmulBTKernel
// consumes it; a staged device weight is never elem-repacked.
t.elem_kn_repacked = w.elem_kn_repacked;
return t;
}
// AUDIT GUARD (KERNEL-GEMM-CPU-TILED lever 2). Only the CPU MatmulBTKernel
// honours elem_kn_repacked, and the staging path below uploads bytes verbatim
// and returns a tensor WITHOUT the marker, so a repacked weight reaching a
// staged device would be read as plain [N,K] and produce garbage silently.
// VT_CPU_ELEM_KN_REPACK is CPU-only and the loader policy cannot see the
// device, so this is where the invariant is enforced: fail loudly at load
// rather than corrupt tokens at inference.
VT_CHECK(!w.elem_kn_repacked,
"qwen3_5: an elem_kn_repacked ([K,N]) weight reached device staging; "