forked from HuangPuStar/MetaInfer
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathinference_blueprint.json
More file actions
2939 lines (2939 loc) · 193 KB
/
Copy pathinference_blueprint.json
File metadata and controls
2939 lines (2939 loc) · 193 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
{
"metadata": {
"name": "agent-infer TP/EP Inference Blueprint",
"version": "2.3.0",
"last_updated": "2026-04-30",
"scope": "Framework + Qwen3 TP + DeepSeek-V2 TP/EP",
"purpose": "给 Agent 提供可直接执行的推理框架搭建知识与 TP 适配约束。"
},
"agent_navigation": {
"ref_docs_policy": {
"rule": "ref_docs/source_refs 路径中的文档是蓝图信息的扩展存储,不是被动参考资料。当蓝图的 pseudocode/API spec 引用 ref_doc 章节时,该章节包含的信息视为蓝图的组成部分。Agent 在实现前必须实际打开并验证引用存在,不得假定'文档存在即信息完备'。",
"mandatory": [
"改动涉及 ref_docs 的组件前,必须先打开并阅读对应的 ref_doc 文件",
"审计时必须抽样打开至少 3 个 ref_doc,逐段验证至少 1 个引用的知识点确实存在",
"发现 ref_doc 与蓝图矛盾时以蓝图为准并记录差异",
"ref_doc 中找不到蓝图引用的知识点时标记为信息断裂,不得脑补"
]
},
"how_to_find_tp_knowledge": "Qwen 与 DeepSeek-V2 的并行与层间契约分两处:routing/算子语义在 model_layer.architecture_knowledge_base;具体 shape 在 framework_layer.data_flow_contracts.tp_layer_interface_contracts。实现代码入口见各 source_impl / impl_code 字段。",
"qwen3_dense_tp": {
"json_paths": [
"model_layer.architecture_knowledge_base.qwen_series_dense",
"framework_layer.data_flow_contracts.tp_layer_interface_contracts.qwen3_tp_model_interfaces"
],
"impl_entrypoints": [
"engine/models/qwen.py",
"engine/tp_layers/linear.py",
"engine/tp_layers/embedding.py",
"llm_engine.py"
]
},
"deepseek_v2_mla_moe_tp": {
"json_paths": [
"model_layer.architecture_knowledge_base.deepseek_v2_v3_mla_moe",
"model_layer.lazy_loader_synthesis_rules.deepseek_mla_moe_loader",
"framework_layer.data_flow_contracts.tp_layer_interface_contracts.deepseek_v2_tp_model_interfaces",
"framework_layer.data_flow_contracts.tp_layer_interface_contracts.deepseek_v2_mla_kv_contract"
],
"impl_entrypoints": [
"engine/models/deepseek_v2.py",
"engine/tp_layers/moe.py",
"engine/tp_layers/linear.py",
"engine/tp_layers/embedding.py",
"llm_engine.py"
]
},
"execution_policy": {
"single_agent_only": true,
"monitor_subagent_exception": "允许 1 个只读监控子代理,仅采集 HCU/VRAM 指标,不得改代码。",
"note": "禁止并行 subagents 改同一套文件;长任务分 phase 执行,默认目标是一次会话完成 phase_1~phase_9(见 AGENT_SKILL.md)。"
}
},
"framework_layer": {
"project_context": {
"problem_statement": "成熟推理框架过于臃肿,包含大量多模型、多部署、多策略动态分派,降低可维护性与性能。",
"goal": "先搭建最小可用推理框架组件,再完成 Qwen3 与 DeepSeek-V2 的 TP 推理闭环。"
},
"components": [
{
"name": "Scheduler",
"role": "执行 prefill/decode 连续批调度;prefill 优先;资源不足时只排队不抢占;在 postprocess 中推进状态并触发 token 级块扩展。",
"ref_docs": [
"notebooks-cn/01_framework_design/02_scheduler.md",
"notebooks-cn/01_framework_design/07_request_lifecycle.md"
],
"ref_code": [
"ref_projects/nano-vllm/nanovllm/engine/scheduler.py"
],
"impl_code": [
"engine/scheduler.py"
],
"_dual_track_note": "LLMEngine block_size=16 仅对 RealModelRunner 有效。TP Runner 硬编码 _kv_block_size=256。",
"_nano_vllm_override": "nano-vllm scheduler.py 的 preempt() 逻辑 (line 52-57) 必须删除。block_size 所有引用必须替换为 self._block_size (由 LLMEngine 注入 16 或 256)。 nano-vllm scheduler.py line 52 can_append(seq) 和 line 60 may_append(seq) 需改为 can_append_one_more (num_free_blocks>=1) 逻辑。TP 路径下 BlockManager 已降级,不能直接调用其 can_append/may_append 方法。 preempt() 方法体 (L66-69) 完全删除。deallocate() 方法在 TP 路径下改为 no-op。schedule() 中 L54 self.running.pop() 替换为顺序迭代 (pop + re-append 在无抢占 TP 路径会导致序列丢失)。 TP 路径下 can_append_one_more = (runner.get_num_free_blocks() >= 1)。TP 路径不调用 BlockManager 的 can_append/may_append (已降级 no-op)。decode 调度使用 runner 来源的 num_free_blocks。 TP 路径下 Schedule.schedule() 方法体必须重写(非外部包装)。因为 nano-vllm 内部有 3 处 self.block_manager.xxx() 调用,全部需替换为 runner.get_num_free_blocks() 来源的 num_free 参数。参考 scheduler_tp_runner_bridge.prefill_timing_gap.pseudocode 的 schedule(num_free) 签名。"
},
{
"name": "KVMemoryPool",
"role": "管理 KV 逻辑块容量、块分配/释放与可扩展性检查;基于模型配置估算 KV 字节预算;提供可选 GPU KV 占位张量。",
"ref_docs": [
"notebooks-cn/01_framework_design/03_kv_cache.md",
"notebooks-cn/01_framework_design/06_memory_pool.md"
],
"ref_code": [
"ref_projects/nano-vllm/nanovllm/engine/block_manager.py",
"ref_projects/nano-vllm/nanovllm/engine/model_runner.py"
],
"impl_code": [
"engine/memory_pool.py",
"engine/kv_specs.py"
],
"_responsibility_boundary": "KVMemoryPool: 仅显存预算(estimate_num_blocks)+GPU placeholder。BlockManager: 运行时分配/释放/prefix caching+get_num_free_blocks() API。",
"tp_path_note": "TP Runner 路径下 KVMemoryPool 仅用于 LLMEngine.__init__ 时调用 estimate_num_blocks() 做显存预算日志。实际 KV cache tensor (_key_cache/_value_cache) 由 QwenAttentionTP 内部 torch.zeros 创建 (见 paged_kv_cache_contract.kv_cache_format.initialization)。TP 路径不使用 KVMemoryPool 的 GPU placeholder——这些 placeholder 仅为 RealModelRunner HF 路径预留。",
"_nano_vllm_override": "TP 路径下禁止调用 KVMemoryPool 的 GPU placeholder 创建逻辑 (nano-vllm model_runner.py 的 allocate_kv_cache 模式)。实际 KV cache 由 QwenAttentionTP 内部 torch.zeros 创建。"
},
{
"name": "BlockManager",
"role": "分页块分配器;维护 free/used block 集合;通过链式哈希执行 prefix caching 与 ref_count 共享;支持 may_append 增量扩展。",
"ref_docs": [
"notebooks-cn/01_framework_design/03_kv_cache.md"
],
"ref_code": [
"ref_projects/nano-vllm/nanovllm/engine/block_manager.py"
],
"impl_code": [
"engine/block_manager.py"
],
"api_spec": {
"compute_hash": {
"signature": "compute_hash(token_ids: tuple[int,...]) -> int",
"purpose": "prefix caching — 相同前缀共享 KV block",
"algorithm_detail": "hash(tuple(token_ids[block_start:block_start+block_size])) — Python builtin hash 跨进程一致。hash→block_id 映射表实现 prefix caching。",
"hash_structure": "dict[int, int] — hash_value → block_id。冲突时沿用现有 block(不链式)。prefix caching 查找: hash_id = hash(prefix_tokens); if hash_id in table: ref_count[block_id] += 1; return block_id",
"hash_policy": "推荐使用 Python builtin hash(tuple(token_ids)),当前单进程部署足够。跨进程场景使用 hashlib.md5。默认实现选 Python hash。",
"_nano_vllm_override": "nano-vllm 使用 xxhash.xxh64()+numpy.tobytes(),蓝图使用 Python builtin hash(tuple())。TP 路径不使用 compute_hash (prefix caching disabled),此差异无功能影响。实现时选 Python hash 即可。"
},
"allocate": {
"signature": "allocate(seq, num_blocks) -> list[int]",
"side_effect": "ref_count += 1 per block"
},
"free": {
"signature": "free(block_id) -> None",
"algorithm": "ref_count -= 1; if 0: return to free_pool"
},
"may_append": {
"signature": "may_append(seq) -> bool",
"formula": "num_free_blocks >= 1"
},
"ref_count_lifecycle": [
"+1: allocate 时",
"+1: prefix cache 命中时",
"-1: Sequence FINISHED 时 free()"
],
"get_num_free_blocks": {
"signature": "get_num_free_blocks() -> int",
"formula": "len(free_pool)"
}
},
"_nano_vllm_override": "TP 路径下 BlockManager 降级为纯计数器。allocate/free 改为 no-op (仅 count+=1/count-=1)。get_num_free_blocks() 保留原逻辑 (len(free_pool)) 但在 LLMEngine 中改为调用 runner.get_num_free_blocks()。compute_hash/may_append 在 TP 路径下不被调用。",
"_tp_degradation_fork_interface": {
"description": "BlockManager 的 TP 降级通过 LLMEngine.__init__ 中的 inference_backend 路由实现条件分叉。不推荐继承/子类化——使用同一类,在方法开头检查 self._tp_mode 标志。",
"implementation": [
"class BlockManager:",
" def __init__(self, ..., tp_mode=False):",
" self._tp_mode = tp_mode",
" self._free_pool = set(range(num_blocks))",
" self._ref_count = {}",
" def allocate(self, seq, num_blocks):",
" if self._tp_mode: return list(range(num_blocks)) # 假分配,返回占位符",
" # ... normal logic ...",
" def free(self, block_id):",
" if self._tp_mode: return # no-op",
" # ... normal logic ...",
" def get_num_free_blocks(self):",
" return len(self._free_pool) # 两种模式均可用"
],
"injection": "LLMEngine.__init__: self.block_manager = BlockManager(tp_mode=(inference_backend in ('qwen_tp','deepseek_tp')))"
}
},
{
"name": "ModelRunner",
"role": "加载 tokenizer 与模型 Runner;执行 prefill/decode 前向并调用采样器输出 next token。",
"ref_docs": [
"notebooks-cn/01_framework_design/04_model_runner.md",
"notebooks-cn/01_framework_design/07_request_lifecycle.md",
"notebooks-cn/07_improvementPlan/improvement_plan.md",
"notebooks-cn/07_improvementPlan/qwen3_effective_changes.md"
],
"ref_code": [
"ref_projects/nano-vllm/nanovllm/engine/model_runner.py"
],
"impl_code": [
"llm_engine.py::RealModelRunner",
"engine/models/qwen.py::QwenTPModelRunner",
"engine/models/deepseek_v2.py::DeepseekTPModelRunner"
],
"tp_runner_actual_flow": {
"_note": "TP Runner (QwenTPModelRunner/DeepseekTPModelRunner) 不走 blueprint 旧 HF 路径;实际使用 paged KV cache + 自定义 TP attention + flash_attn_with_kvcache",
"prefill": "model.forward(input_ids, past_key_values=None) → embed_tokens → layer.forward() (含 prefill KV allocation + flash_attn_varlen_func)",
"decode": "model.forward(input_ids, past_key_values=kv_lens) → embed_tokens → 逐层 layer.forward_decode(hidden_states, pos, kv_len, residual=residual)",
"kv_lens_update": "decode 后 batch 读取 layer.self_attn._kv_len_gpu[0].item() — 必须在 compiled region 外部执行",
"sampling": "sample_next_tokens(logits[:, -1, :], temperature, top_p) → 返回 token ids",
"engine_integration": "num_free = block_mgr.get_num_free_blocks() or runner.get_num_free_blocks(); batch, is_prefill = scheduler.schedule(num_free); tokens = runner.run(batch, is_prefill=is_prefill, temperature=temperature, top_p=top_p); scheduler.postprocess(batch, is_prefill, generated_tokens=tokens)",
"decode_batch_mode": "当前 B=1 单序列。多序列时逐序列 for-loop 调用 forward_decode(非 batch tensor)。因 _block_table 和 _kv_len_gpu 各序列不同值,batch 需扩展所有 attention 层为 [B,...] 数据结构。",
"get_num_free_blocks": "QwenTPModelRunner 必须暴露 get_num_free_blocks() -> int 方法。实现见 scheduler_tp_runner_bridge.num_free_blocks_source.TP_Runner.impl。",
"run_method_impl": [
"def run(self, seqs, is_prefill, temperature, top_p):",
" if not seqs: return []",
" if is_prefill:",
" input_ids = torch.cat([s.input_ids_tensor() for s in seqs], dim=1) # [1,total_tokens]",
" positions = torch.cat([torch.arange(s.seq_len(),device=self.device) for s in seqs])",
" logits, kv_lens = self.model(input_ids, past_key_values=None, position_offset=0, max_seq_len=self.max_seq_len)",
" for s in seqs: s.kv_len = s.seq_len()",
" else:",
" kv_lens = [s.kv_len for s in seqs]",
" input_ids = torch.tensor([[s.output_ids[-1]] for s in seqs], dtype=torch.long, device=self.device)",
" logits, new_kv_lens = self.model(input_ids, past_key_values=kv_lens, position_offset=seqs[0].kv_len, max_seq_len=self.max_seq_len)",
" for s,l in zip(seqs, new_kv_lens): s.kv_len = l",
" tokens = self._sample(logits[:,-1,:], temperature, top_p) # TP: rank 0 only + broadcast",
" return tokens"
],
"_nano_vllm_override": "删除 nano-vllm 的 model(input_ids, use_cache=False, return_dict=True) 调用模式。TP Runner prefill 调用 model.forward(input_ids, past_key_values=None),decode 调用 model.forward(input_ids, past_key_values=kv_lens)。不使用 HF 的 use_cache/return_dict 语义。",
"_nano_vllm_per_function": {
"allocate_kv_cache()": "DELETE — KV 由 QwenAttentionTP 自管",
"prepare_prefill()": {
"status": "PARTIAL — KEEP vs REPLACE 精确边界",
"keep": [
"L129-150: cu_seqlens 构造逻辑 — 逐序列累加 seq_len,构造 cu_seqlens_q/cu_seqlens_k",
"L129-150: positions 逐序列 torch.arange(seq_len)",
"L129-150: input_ids torch.cat concatenation"
],
"replace": [
"L154-162: nano-vllm contiguous slot_mapping (block_table[i]*block_size + offset → range(slot_start, slot_end))",
"→ 替换为蓝图 paged_slot_mapping_algorithm: block_table[0, i//256] * 256 + (i%256) per-token 计算",
"block_size 16→256, slot_mapping dtype int32→int64, 数据结构 list[int]→Tensor[1,max_blocks] int32"
],
"integration_note": "保留的 cu_seqlens 代码与替换的 slot_mapping 代码之间的集成由重建者编写。cu_seqlens 依赖 seq.seq_len() 列表;slot_mapping 依赖 block_table Tensor。这两个数据结构的来源不同,集成点需注意 device 和 dtype 一致性。"
},
"prepare_decode()": "DELETE — 完全不同的 contiguous vs paged slot_mapping",
"capture_cudagraph()": "DELETE — 不在此次重构范围",
"run_model()": "DELETE — 替换为 QwenTPModelRunner.run() 的 forward_decode 调用",
"IPC SharedMemory (L43-48)": "DELETE — TP Runner 不使用 IPC SharedMemory"
}
}
},
{
"name": "Sampler",
"role": "根据 temperature/top_p 执行 greedy 或随机采样,将 logits 转换为 token ids。",
"ref_docs": [
"notebooks-cn/01_framework_design/05_sampler.md"
],
"ref_code": [
"ref_projects/nano-vllm/nanovllm/layers/sampler.py"
],
"impl_code": [
"engine/sampler.py"
],
"tp_sampling_protocol": {
"hard_rule": "TP 多卡仅 rank 0 采样,broadcast 给所有 rank。严禁各 rank 独立采样。",
"pseudocode": [
"if world_size > 1:",
" if rank == 0: tokens = [sample(logits[i,-1,:]) for i in range(B)]",
" else: tokens = [0] * B",
" for i,t in enumerate(tokens):",
" tt = torch.tensor([t], dtype=torch.long, device=device); dist.broadcast(tt, src=0)",
" tokens[i] = tt.item()",
"else: tokens = [sample(logits[i,-1,:]) for i in range(B)]"
],
"_nano_vllm_override": "nano-vllm sampler.py 是单卡实现。TP 模式下在 runner._sample() 中包裹 if world_size>1 分支 (rank 0 采样 + broadcast),不修改 sampler.py 本身。broadcast 使用 src=0 参数。"
}
},
{
"name": "Sequence",
"role": "请求级状态容器:维护 input/output token、block_table、缓存 token 计数、状态机转移与分块视图。",
"ref_docs": [
"notebooks-cn/01_framework_design/01_architecture.md",
"notebooks-cn/01_framework_design/07_request_lifecycle.md"
],
"ref_code": [
"ref_projects/nano-vllm/nanovllm/engine/sequence.py"
],
"impl_code": [
"engine/structs.py"
]
},
{
"name": "LLMEngine",
"role": "系统编排器:路由 inference_backend → 创建 Runner → 估算 KV 池 → 初始化 Scheduler,驱动 while-loop / step-loop 完成生成。是 Scheduler 与 ModelRunner 之间的胶水层。",
"ref_docs": [
"notebooks-cn/01_framework_design/01_architecture.md",
"notebooks-cn/01_framework_design/07_request_lifecycle.md"
],
"ref_code": [
"ref_projects/nano-vllm/nanovllm/engine/llm_engine.py"
],
"impl_code": [
"llm_engine.py::LLMEngine"
],
"full_api_surface": {
"__init__": {
"signature": "__init__(self, model_dir, inference_backend='hf', block_size=16, mem_utilization=0.85, reserve_bytes=2*1024**3, max_num_seqs=4, max_num_batched_tokens=4096)",
"flow": [
"1. torch.cuda.set_device(local_rank); self.device=torch.device(f'cuda:{local_rank}'); self.dtype=torch.bfloat16",
"2. 路由 inference_backend: 'tp' → _select_tp_backend(model_dir) 自动检测 Qwen/DeepSeek; 'qwen_tp'/'deepseek_tp'/'hf' 直接使用",
"3. 创建 Runner: QwenTPModelRunner / DeepseekTPModelRunner / RealModelRunner (HF fallback)",
"4. self.eos_token_id = self.runner.tokenizer.eos_token_id",
"5. num_blocks = self._estimate_kv_blocks() # 基于剩余显存 + block_size 估算",
"6. self.memory_pool = KVMemoryPool(num_blocks, block_size, ...) # TP 路径仅做预算日志",
"7. self.scheduler = Scheduler(memory_pool, max_num_seqs, max_num_batched_tokens)"
],
"_select_tp_backend": "读取 model_dir/config.json architectures[0] → Qwen2/3→'qwen_tp', DeepseekV2/3→'deepseek_tp', else raise ValueError",
"_estimate_kv_blocks": {
"_note": "KVMemoryPool.estimate_num_blocks 仅实现 MLA 公式。Qwen3 Dense 模型需独立实现。",
"dense_pseudocode": [
"def _estimate_kv_blocks(self):",
" free_bytes, total = torch.cuda.mem_get_info(device=self.device)",
" cfg = self.runner.model.config",
" if hasattr(cfg, 'qk_nope_head_dim'): # MLA (DeepSeek)",
" return KVMemoryPool.estimate_num_blocks(cfg, block_size=self.block_size, dtype=self.dtype, free_bytes=free_bytes, reserve_bytes=self.reserve_bytes, mem_utilization=self.mem_utilization)",
" # Dense (Qwen): K+V per token = layers * kv_heads * head_dim * 2 * elem_bytes",
" elem = 2 if self.dtype in (torch.float16, torch.bfloat16) else 4",
" kv_head_dim = int(getattr(cfg, 'head_dim', cfg.hidden_size // cfg.num_attention_heads))",
" kv_heads = int(getattr(cfg, 'num_key_value_heads', cfg.num_attention_heads))",
" layers = int(cfg.num_hidden_layers)",
" bytes_per_token = layers * kv_heads * kv_head_dim * 2 * elem # K+V",
" bytes_per_block = bytes_per_token * self.block_size",
" budget = max(0, int((free_bytes - self.reserve_bytes) * self.mem_utilization))",
" return max(1, budget // max(bytes_per_block, 1))"
]
}
},
"generate": {
"signature": "generate(self, prompt: str|list[str], max_new_tokens: int, temperature=0.0, top_p=None) → str|list[str]",
"flow": [
"1. _enqueue(prompts): tokenizer.encode → Sequence(input_ids, sampling_params={max_tokens,temperature,top_p}) → seq.block_size=self.block_size → scheduler.add_request(seq)",
"2. while-loop: step+=1; batch,is_prefill=scheduler.schedule()",
"3. if not batch: 检查 _all_finished(seqs) → break; else raise 'empty batch before all finished'",
"4. if is_prefill: first_tokens=runner.run(batch,True,temp,top_p); scheduler.postprocess(batch,True,first_tokens)",
"5. else: next_tokens=runner.run(batch,False,temp,top_p); scheduler.postprocess(batch,False,next_tokens); 逐 seq _finish_check_and_cleanup",
"6. tokenizer.decode(seq.output_ids) → return text"
],
"_finish_check_and_cleanup": "检查 seq.output_ids 是否达到 EOS 或 max_tokens → 调用 seq.transition_to(FINISHED) → scheduler.running.remove(seq) → memory_pool.free_sequence(seq) → seq.past_key_values=None。返回 True/False。",
"_all_finished": "all(seq.status == SequenceStatus.FINISHED for seq in seqs)"
},
"step": {
"signature": "step(self, temperature=0.0, top_p=None) → list[Sequence]",
"purpose": "单步推进(供 OpenAI server / has_unfinished_requests 循环使用)",
"flow": [
"1. batch, is_prefill = self.scheduler.schedule()",
"2. if not batch: 清理 running 中已达 max_tokens 的 seq (_finish_check_and_cleanup) → return finished",
"3. if is_prefill: tokens=runner.run(batch,True,...); scheduler.postprocess(batch,True,tokens)",
"4. else: tokens=runner.run(batch,False,...); scheduler.postprocess(batch,False,tokens); 逐 seq _finish_check_and_cleanup → finished.append(seq)",
"5. return finished"
]
},
"begin_generation": {
"signature": "begin_generation(self, prompts, max_new_tokens, temperature, top_p) → None",
"purpose": "批量加入 prompt 到调度器,配合 has_unfinished_requests()+step() 分步推进(OpenAI server 使用)"
},
"has_unfinished_requests": {
"signature": "has_unfinished_requests(self) → bool",
"purpose": "检查 scheduler.waiting/running 和 _active_gen_seqs 是否还有未完成序列"
},
"get_generation_outputs": {
"signature": "get_generation_outputs(self) → list[str]",
"purpose": "返回 _active_gen_seqs 的 decode 文本"
},
"_enqueue": {
"signature": "_enqueue(self, prompts, max_new_tokens, temperature, top_p, request_ids=None) → list[Sequence]",
"flow": "tokenizer.encode → Sequence(input_ids, sampling_params) → seq.block_size=self.block_size → scheduler.add_request(seq)"
}
}
},
{
"name": "OpenAITPServer",
"role": "OpenAI 兼容 HTTP API 服务。基于 ThreadingHTTPServer 提供 /v1/completions 端点,封装 LLMEngine.generate/step API,支持 TP 多卡同步(broadcast 机制)和 streaming 输出。用于 benchmark 压测。",
"impl_code": [
"openai_tp_server.py"
],
"_phase": "phase_10 — E2E 验收的 benchmark 接口。Phase 9 完成后才可构建。",
"architecture": {
"server_type": "ThreadingHTTPServer(BaseHTTPRequestHandler)",
"endpoints": [
"GET /health",
"POST /v1/completions"
],
"tp_sync_mechanism": {
"description": "TP 多卡场景下所有 rank 按严格相同顺序执行 collective。non-rank0 等待 broadcast_obj 命令,rank0 处理 HTTP 请求后广播。",
"rank0_flow": "接收请求 → broadcast_obj({action,prompt,max_tokens,...}) → engine.generate() → 返回响应",
"non_rank0_flow": "while True: cmd=broadcast_obj({}); if shutdown→break; 执行相同 engine.generate()/generate_stream()",
"request_lock": "threading.Lock 序列化所有请求。并行请求导致跨 rank NCCL collective 顺序不一致 → 超时/死锁。",
"broadcast_obj": "dist.broadcast_object_list([payload if rank0 else None], src=0) → 所有 rank 返回 payload[0]"
},
"streaming": {
"flow": "engine.generate_stream() → SSE data chunks → final chunk finish_reason='stop' → data: [DONE]"
},
"non_streaming": {
"flow": "engine.generate() → JSON response with choices/text/usage"
}
},
"startup_sequence": [
"1. parse_args: --model-dir, --backend(tp/qwen_tp/deepseek_tp/hf), --host, --port, --max-num-seqs, --max-num-batched-tokens, --max-new-tokens-cap",
"2. init_dist_if_needed(): WORLD_SIZE>1 时 dist.init_process_group('nccl')",
"3. LLMEngine(model_dir, inference_backend, max_num_seqs, max_num_batched_tokens)",
"4. run_tp_generation_loop(engine, host, port, max_new_tokens_cap)"
],
"benchmark_usage": {
"start_server": "TP_SIZE=4 PORT=9000 bash start_tp_infer_service.sh dsv2",
"run_benchmark": "PORT=9000 NUM_PROMPTS=50 REQUEST_RATE=1 MAX_CONCURRENCY=1 bash run_myengine_benchmark.sh dsv2",
"key_metrics": [
"Output throughput (tok/s)",
"Mean TTFT (ms)",
"Duration (s)"
]
}
}
],
"data_flow_contracts": {
"request_level": {
"sequence_fields": {
"input_ids": "list[int]",
"output_ids": "list[int]",
"token_ids": "input_ids + output_ids",
"block_table": "list[int] (HF 路径) 或 torch.Tensor [1, max_blocks] int32 (TP Runner 路径)。Sequence 类需同时支持两种表示。",
"num_cached_tokens": "int",
"status": "WAITING|RUNNING_PREFILL|RUNNING_DECODE|FINISHED",
"block_table_dual_repr": {
"hf_path": "list[int] — BlockManager 分配的逻辑 block ID 列表",
"tp_path": "torch.Tensor [1, max_blocks] int32 — QwenAttentionTP 自管。prefill 时 torch.arange 填入,decode 时不变。",
"switch_logic": {
"_switch_mechanism": "通过 LLMEngine.__init__ 传入 max_blocks 和 device 参数。Sequence 构造时不区分 HF/TP 路径——双轨数据结构同时初始化,由调用方根据 inference_backend 选择调用哪个方法。",
"construction": [
"# LLMEngine.__init__:",
"config = json.load(open(model_dir / 'config.json'))",
"max_blocks = config['max_position_embeddings'] // 256 # 40960//256=160",
"device = torch.device(f'cuda:{local_rank}')",
"# 传入 Sequence 构造:",
"class Sequence:",
" def __init__(self, ..., max_blocks=None, device=None):",
" self._block_table_list = [] # HF 路径: list[int]",
" self._max_blocks = max_blocks # TP 路径需要",
" self._device = device",
" self._block_table_tensor = None # 惰性初始化",
" def block_table_tensor(self): # TP Runner 调用",
" if self._block_table_tensor is None:",
" self._block_table_tensor = torch.zeros(1, self._max_blocks, dtype=torch.int32, device=self._device)",
" return self._block_table_tensor",
" def block_table_list(self): # HF Runner 调用",
" return self._block_table_list"
],
"caller_side_routing": [
"# TP Runner: seq.block_table_tensor() — 返回 Tensor[1,max_blocks] int32",
"# HF Runner: seq.block_table_list() — 返回 list[int]",
"# 切换不是通过 Sequence 内部 if-else,而是调用方知道自己的路径并调用对应方法。",
"# Sequence 不需要知道 inference_backend——它只是数据的被动词典。"
]
}
}
},
"status_transitions": [
"WAITING -> RUNNING_PREFILL",
"RUNNING_PREFILL -> RUNNING_DECODE",
"RUNNING_DECODE -> FINISHED"
]
},
"scheduler_to_runner": {
"batch_type": "list[Sequence]",
"prefill_trigger": "schedule() 优先从 waiting 组 batch;必须满足 max_num_batched_tokens 与 can_allocate",
"decode_trigger": "waiting 无可调度项时,从 running 中选择 can_append_one_more 的序列",
"preemption_policy": "disabled; 资源不足时不抢占,只等待",
"max_num_batched_tokens": {
"formula": "max(1, num_free_blocks * block_size)",
"block_size_framework": 16,
"block_size_tp_runner": 256,
"block_size_selection": {
"hf": 16,
"qwen_tp": 256,
"deepseek_tp": 256,
"injection_point": "LLMEngine.__init__ → Scheduler._block_size (见 scheduler_tp_runner_bridge)"
}
},
"can_allocate": {
"formula": "num_free_blocks >= seq.required_blocks()",
"required_blocks": "ceil(len(seq.input_ids) / block_size)"
},
"can_append_one_more": {
"formula": "num_free_blocks >= 1"
},
"schedule_algorithm": {
"phase1_prefill": "从 waiting 队列取 seq,满足 can_allocate 且累计 tokens ≤ max_num_batched_tokens",
"phase2_decode": "waiting 空时从 running 取 seq,满足 can_append_one_more",
"empty_return": "([], False) — 调用方忙等重试或检查 all_finished 退出",
"chunked_prefill_rule": "prefill/decode 不混批。schedule()先prefill后decode。同batch序列同阶段。",
"state_transition_safety": "序列状态在 postprocess 中更新 (PREFILL→DECODE, DECODE→FINISHED)。schedule() 返回的 batch 内所有序列为同一阶段。不存在 prefill/decode 混批竞态——Scheduler 在 schedule() 内部已按阶段分组。",
"overlength_rejection": "enqueue 时检测: seq.required_blocks() > max_blocks → 拒绝或截断。新增 status='REJECTED',已拒绝不参与调度。防止超长 prompt 永久 WAITING 导致死循环。",
"schedule_complete_method": [
"# Scheduler.schedule(num_free) — 完整实现 (B=1, nocompile, TP=4)",
"class Scheduler:",
" def __init__(self, memory_pool, max_num_seqs, max_num_batched_tokens):",
" self.waiting = [] # list[Sequence] — 待 prefill",
" self.running = [] # list[Sequence] — 运行中 (decode 阶段)",
" self.memory_pool = memory_pool",
" self.max_num_seqs = max_num_seqs",
" self.max_num_batched_tokens = max_num_batched_tokens",
" self._block_size = 16 # LLMEngine 注入: TP→256, HF→16",
" self._reserved_blocks = 0",
" if req > self._max_blocks: # e.g. (40960+255)//256 = 160, injected by LLMEngine",
" def schedule(self, num_free):",
" \"\"\"num_free: BlockManager.get_num_free_blocks() or runner.get_num_free_blocks()\"\"\"",
" # Phase 1: prefill (prefill-first policy)",
" batch = []",
" reserved = 0",
" current_tokens = 0",
" for seq in list(self.waiting): # iterate copy, may remove REJECTED",
" req = (len(seq.input_ids) + self._block_size - 1) // self._block_size",
" # Check REJECTED: prompt too long",
" if req > max_blocks_for_model: # e.g. (40960+255)//256 = 160",
" seq.transition_to(SequenceStatus.REJECTED)",
" self.waiting.remove(seq)",
" continue",
" # Can allocate?",
" if reserved + req > num_free:",
" break # insufficient free blocks, stop prefill for this round",
" if current_tokens + len(seq.input_ids) > self.max_num_batched_tokens:",
" break # token budget exhausted",
" batch.append(seq); reserved += req",
" current_tokens += len(seq.input_ids)",
" if batch:",
" self._reserved_blocks += reserved",
" return [s for s in batch], True # (batch, is_prefill=True)",
" # Phase 2: decode",
" for seq in list(self.running):",
" if num_free - reserved >= 1: # can_append_one_more",
" batch.append(seq); reserved += 1",
" if batch:",
" self._reserved_blocks += reserved",
" return [s for s in batch], False # (batch, is_prefill=False)",
" # Empty: no schedulable work",
" return [], False"
],
"max_blocks_for_model": {
"definition": "max_blocks_for_model = config.max_position_embeddings // block_size # e.g. 40960//256=160",
"source": "由 LLMEngine.__init__ 从 config.json 读取后注入 Scheduler: self.scheduler._max_blocks = config.max_position_embeddings // 256",
"injection_code": "# in LLMEngine.__init__: self.scheduler._max_blocks = config.max_position_embeddings // 256"
}
},
"batch_assembly_contract": {
"prefill_ragged": {
"input_ids": "torch.cat([seq.input_ids_tensor() for seq in batch], dim=1) # [1, total_tokens] ragged concat",
"positions": "torch.cat([torch.arange(len(seq), device=device) for seq in batch]) # [total_tokens]",
"block_tables": "[seq.block_table_tensor() for seq in batch] # 每序列独立 block_table",
"note": "TP Runner 使用 ragged concatenation,非 padding。embedding 直接接受 [1, total_tokens]"
},
"decode_single": {
"input_ids": "torch.tensor([[seq.output_ids[-1]]], dtype=torch.long, device=device) # [1, 1] 最新 token",
"note": "decode 每步只处理最新 token,batch 中多序列分别调用 forward_decode"
}
},
"postprocess_complete_method": [
"# Scheduler.postprocess(batch, is_prefill, tokens) — 完整实现",
"def postprocess(self, batch, is_prefill, tokens):",
" for i, seq in enumerate(batch):",
" if is_prefill:",
" # Prefill: write first token to output_ids",
" seq.output_ids.append(tokens[i])",
" seq.status = SequenceStatus.RUNNING_DECODE",
" if seq not in self.running: self.running.append(seq)",
" else: # decode",
" seq.output_ids.append(tokens[i])",
" # Check finish conditions",
" if tokens[i] == self.eos_token_id:",
" seq.status = SequenceStatus.FINISHED",
" self._release(seq)",
" elif len(seq.output_ids) >= seq.sampling_params.get('max_tokens', 0):",
" seq.status = SequenceStatus.FINISHED",
" self._release(seq)",
" def _release(self, seq):",
" if seq in self.running: self.running.remove(seq)",
" self.memory_pool.free_sequence(seq) # TP 路径: no-op or release KV blocks",
" seq.block_table = [] # 清空",
" self._reserved_blocks = 0 # 重置计数器 (当前单序列 B=1 简单处理)"
]
},
"runner_prefill_tensors": {
"_note": "以下为 RealModelRunner(HF 兜底路径)的契约。QwenTPModelRunner / DeepseekTPModelRunner 使用自定义 TP attention + paged KV cache,不走此路径。",
"input_ids": {
"shape": "[1, L_prompt] per sequence",
"dtype": "torch.int64",
"device": "cuda"
},
"forward_call": "model(input_ids=ids, use_cache=False, return_dict=True) <- 仅 RealModelRunner 使用",
"logits_shape": "[1, L_prompt, vocab_size]",
"sampling_input": {
"shape": "[1, vocab_size]",
"source": "logits[0, -1, :].unsqueeze(0)"
},
"sampling_output": {
"shape": "[1]",
"dtype": "torch.int64"
}
},
"runner_decode_tensors": {
"_note": "以下为 RealModelRunner(HF 兜底路径)的契约。TP Runner 的 prefill/decode 见 tp_layer_interface_contracts.qwen3_tp_model_interfaces.decode_forward_pattern。",
"input_ids": {
"shape": "[B, L_max] (left padding)",
"dtype": "torch.int64",
"device": "cuda"
},
"attention_mask": {
"shape": "[B, L_max]",
"dtype": "torch.int64",
"device": "cuda",
"rule": "pad=0, valid_token=1"
},
"forward_call": "model(input_ids=ids, attention_mask=m, use_cache=False, return_dict=True) <- 仅 RealModelRunner 使用",
"logits_shape": "[B, L_max, vocab_size]",
"sampling_input": {
"shape": "[B, vocab_size]",
"source": "logits[:, -1, :]"
},
"sampling_output": {
"shape": "[B]",
"dtype": "torch.int64"
},
"_tp_runner_actual_path": {
"input": "[1,1,hidden_size] (仅最新 token embedding)",
"layer_call": "逐层 layer.forward_decode(hidden_states, positions, kv_len, max_seq_len, residual=residual)",
"kv_write": "index_copy_ 到 paged KV cache [_num_blocks, 256, heads, dim]",
"kv_read": "flash_attn_with_kvcache(q, _key_cache, _value_cache, _kv_len_gpu, _block_table, scale, causal=False)",
"causal_rule": "prefill causal=True, decode causal=False (past_key_values is None → prefill)",
"kv_len_tracking": "_kv_len_gpu GPU tensor (非 Python int), .item() 在非编译 forward() 中读取"
}
},
"paged_kv_cache_contract": {
"_note": "P0 增量 KV Cache — TP Runner 实际使用 paged KV cache,非 contiguous buffer。block_size=256(flash_attn_with_kvcache 最低要求)。",
"source_impl": [
"engine/models/qwen.py::QwenAttentionTP"
],
"source_refs": [
"notebooks-cn/07_improvementPlan/improvement_plan.md §P0",
"notebooks-cn/07_improvementPlan/qwen3_effective_changes.md #8"
],
"_ref_doc_contradiction_warning": "improvement_plan.md §P0 (L201-260) 描述的是旧版 HF past_key_values + contiguous KV buffer 方案(use_cache=True, 未分页)。当前蓝图使用 paged KV cache + flash_attn_with_kvcache(分页 buffer [num_blocks,256,heads,dim])。二者架构不兼容。以本蓝图为权威——§P0 仅作为历史参考,不可用于实现。",
"_vg3_fix_verified": "2026-05-27 物理 tracing 确认:prefill 使用 flash_attn_varlen_func + KV cache lazy alloc torch.zeros max_blocks;decode 使用 flash_attn_with_kvcache paged。无 HF past_key_values 机制。",
"kv_cache_format": {
"key_cache": "[num_blocks, 256, num_kv_heads, head_dim] bf16",
"value_cache": "[num_blocks, 256, num_kv_heads, head_dim] bf16",
"block_table": "[1, max_blocks] int32 — 固定 shape,prefill 时分配,decode 时不变",
"kv_len_gpu": "[1] int32 GPU tensor — 追踪当前 KV 长度,非 Python int",
"slot_mapping_decode": "[1] int64 — decode 步写入目标槽位",
"hard_rule": "block_size 必须 >= 256(flash_attn_with_kvcache 硬性要求);block_table 必须 int32",
"max_blocks_formula": {
"calculation": "max_blocks = (config.max_position_embeddings + 256 - 1) // 256",
"qwen3_8b": 128,
"init": "block_table = torch.zeros(1, max_blocks, dtype=torch.int32)",
"dynamic": "有效条目=num_blocks,decode扩展时在[:,num_blocks]追加"
},
"initialization": "num_blocks=(config.max_position_embeddings+255)//256; _key_cache=torch.zeros(num_blocks,256,num_kv_heads,head_dim,dtype=bf16,device=cuda)",
"multi_seq_block_table_expansion": {
"B_1": "_block_table = torch.zeros(1, max_blocks, dtype=torch.int32) # single-sequence (current)",
"B_gt_1": "扩展为 _block_table = torch.zeros(B, max_blocks, dtype=torch.int32)。prefill 时逐序列 block_table[i,:nb] = torch.arange(...)。flash_attn_with_kvcache 按 batch dim 索引各序列 block_table。",
"injection": "QwenForCausalLMTP.forward() prefill 分支中: self._expand_block_table_if_needed(B); for i,layer in enumerate(layers): layer.self_attn._block_table = self._block_table[i:i+1] # per-sequence slice"
},
"_b1_scope": "当前 B=1: _block_table=[1,max_blocks], _kv_len_gpu=[1]。B>1 扩展不在本次构建范围——需 _block_table→[B,max_blocks], _kv_len_gpu→[B], flash_attn_with_kvcache batch dim 索引。"
},
"prefill_kv_write": {
"description": "torch.arange(num_blocks) → block_table → 按 slot_mapping 顺序写入 _key_cache/_value_cache (index_copy_)",
"slot_mapping_algorithm": {
"formula": "slot_mapping[i] = block_table[0, i//256] * 256 + (i%256)",
"pseudocode": [
"num_tokens=input_ids.shape[1]; num_blocks=(num_tokens+255)//256",
"block_table[0,:num_blocks]=torch.arange(num_blocks,dtype=torch.int32,device=device)",
"slot_mapping=torch.zeros(num_tokens,dtype=torch.int64,device=device)",
"for i in range(num_tokens): slot_mapping[i]=block_table[0,i//256].item()*256+(i%256)",
"_key_cache.index_copy_(0,slot_mapping,k_flat)"
],
"multi_seq": [
"offset = 0; next_block_id = 0",
"for seq in batch:",
" n = seq.seq_len(); nb = (n + 255) // 256",
" blk = torch.arange(next_block_id, next_block_id + nb, dtype=torch.int32, device=device)",
" bt_padded = F.pad(blk.unsqueeze(0), (0, max_blocks - nb))",
" seq.block_table = bt_padded",
" for i in range(n):",
" slot_mapping[offset + i] = blk[i // 256].item() * 256 + (i % 256)",
" offset += n; next_block_id += nb"
],
"vectorized": [
"# 向量化替代方案 (避免 .item() GPU sync)",
"num_tokens = input_ids.shape[1]",
"indices = torch.arange(num_tokens, device=device)",
"slot_mapping = block_table[0, indices // 256] * 256 + (indices % 256)",
"# 一行完成, 无需 for 循环 + .item()。短 prompt (<512 tokens) 用 for 循环也可接受",
"# 长 prompt (40K tokens) 必须用向量化,否则 40960 次 .item() ≈ 200ms GPU sync 延迟"
]
},
"full_reshape_chain": {
"pseudocode": [
"# QKV 投影产出 K: [num_tokens, num_kv_heads, head_dim]",
"k_flat = k.reshape(num_tokens, num_kv_heads, head_dim) # ensure contiguous",
"v_flat = v.reshape(num_tokens, num_kv_heads, head_dim)",
"# View KV cache as flat [total_slots, num_kv_heads, head_dim]",
"kc_flat = _key_cache.view(-1, num_kv_heads, head_dim)",
"vc_flat = _value_cache.view(-1, num_kv_heads, head_dim)",
"# index_copy_ on dim=0",
"kc_flat.index_copy_(0, slot_mapping, k_flat)",
"vc_flat.index_copy_(0, slot_mapping, v_flat)",
"# 约束: slot_mapping.max() < num_blocks * 256"
]
},
"integrated_timeline": [
"单序列 prefill:",
" 1. qkv_proj → Q,K,V",
" 2. flash_attn_varlen_func(Q, K, V, cu, causal=True) # K,V 来自投影",
" 3. k_flat = k.reshape(...); kc_flat = _key_cache.view(-1, heads, dim); kc_flat.index_copy_(0, slot_mapping, k_flat)",
"多序列 prefill:",
" 1. 按 multi_seq 构建 slot_mapping 和 block_table",
" 2. qkv_proj → Q,K,V (all tokens)",
" 3. flash_attn_varlen_func(Q, K, V, cu, causal=True)",
" 4. 按 multi_seq 伪代码逐序列 index_copy_ 写入 cache",
"逐层处理:",
" 每层的 QKV 投影和 attention 使用本层投影产出 (非 cache)。逐层 projection→attention→cache_write 顺序执行。"
]
},
"decode_kv_write": "_slot_mapping_decode[0] = _kv_len_gpu[0]; index_copy_ 写入当前 token; _kv_len_gpu[0] += 1",
"decode_attention": "flash_attn_with_kvcache_op(q_reshape, _key_cache, _value_cache, _kv_len_gpu, _block_table, scale, causal=False) softmax_scale=1.0/sqrt(head_dim)",
"decode_kv_len_reading": "kv_lens = [int(l.self_attn._kv_len_gpu[0].item()) for l in self.layers] — 必须在 compiled region 外部执行(.item() 触发 CPU sync)",
"failure_modes": [
"FM-008: block_size < 256 导致 flash_attn_with_kvcache 报错",
"FM-009: .item() 在 compiled region 内触发 SIGABRT"
],
"prefill_failure_rollback": "prefill 期间 RuntimeError → memory_pool.free_sequence(seq) 释放已分配 KV blocks。seq.block_table = []。向上传播异常。"
},
"torch_compile_contract": {
"_note": "P2 torch.compile kernel fusion — Qwen3 TP 使用 per-layer fullgraph=True + reduce-overhead(inductor 内部 CUDA Graph)。",
"source_impl": [
"engine/models/qwen.py::QwenTPModelRunner._setup_cuda_graph_piecewise"
],
"source_refs": [
"notebooks-cn/07_improvementPlan/improvement_plan.md §P2",
"notebooks-cn/07_improvementPlan/qwen3_effective_changes.md #9"
],
"compile_strategy": {
"mode": "reduce-overhead (inductor cudagraph_trees, 对标 vLLM BB-9)",
"fullgraph": true,
"dynamic": true,
"per_layer_compilation": "torch.compile(layer.forward_decode_graph, fullgraph=True, mode='reduce-overhead') — 每层独立编译为一个 FX graph",
"cuda_graph_trigger": "torch._inductor.config.triton.cudagraphs = True (PyTorch 2.9.1 需手动开启)"
},
"forward_decode_design": {
"eager_path": "QwenDecoderLayerTP.forward_decode() — 无 clone,无控制流,torch.compile 兼容",
"graph_path": "QwenDecoderLayerTP.forward_decode_graph() — 开头 clone(hidden_states) + clone(residual),消除 fused_add_rms_norm 的 mutated inputs 警告",
"swap_mechanism": {
"desc": "CUDA_GRAPH=1 时 _setup_cuda_graph_piecewise 将 layer.forward_decode 替换为 torch.compile(forward_decode_graph)",
"implementation": "types.MethodType(torch.compile(layer.forward_decode_graph, fullgraph=True, mode='reduce-overhead', dynamic=True), layer) # 必须用 MethodType 绑定 self,否则 forward_decode(hidden_states,...) 调用缺 self 参数"
}
},
"constraints": {
"no_moe_compile": "MoE 模块有 .item() GPU→CPU 同步 → graph break,不可 fullgraph 编译",
"fixed_shape": "decode 路径必须固定 shape(B=1, S=1),动态 shape 会触发重编译",
"clone_necessity": "forward_decode_graph 的 clone 是 CUDA Graph 正确性必需的 — fused_add_rms_norm 原地修改输入,不 clone 会污染 CUDAGraphEntry 存储的上一层输出"
},
"failure_modes": [
"FM-010: mode='reduce-overhead' 与 KV cache buffer 跨步复用冲突 → RuntimeError",
"FM-011: 动态切片 k[:, :kv_len] 每步 shape 变化 → 编译重编译开销",
"FM-012: 无条件 clone 在 eager 模式造成 ~15% 性能回退"
]
},
"flash_attention_integration_contract": {
"_note": "P3-FA Flash Attention 集成 — Qwen3 prefill 用 flash_attn_varlen_func,decode 用 flash_attn_with_kvcache(custom op 注册为 meta_infer::flash_attn_with_kvcache)。",
"source_impl": [
"engine/models/qwen.py::QwenAttentionTP",
"engine/kernels/custom_ops.py"
],
"source_refs": [
"notebooks-cn/07_improvementPlan/improvement_plan.md §P3-FA",
"notebooks-cn/07_improvementPlan/qwen3_effective_changes.md #10"
],
"prefill_path": {
"kernel": "flash_attn_varlen_func(q, k, v, cu_seqlens_q, cu_seqlens_k, max_seqlen_q, max_seqlen_k, causal=True)",
"qk_format": "[num_tokens, num_heads, head_dim] 3D ragged — 无需 permute",
"kv_source": "paged KV cache 中已写入的 prefill 位置",
"cu_seqlens_construction": {
"cu_seqlens_q": "torch.zeros(len(batch)+1,dtype=torch.int32,device='cuda'); cu_seqlens_q[1:]=torch.tensor([seq.seq_len() for seq in batch]).cumsum(0).to(device)",
"cu_seqlens_k": "cu_seqlens_q.clone()",
"max_seqlen_q": "max(seq.seq_len() for seq in batch)",
"max_seqlen_k": "max_seqlen_q"
},
"kv_source_correction": "正确顺序: 1. QKV投影得Q,K,V → 2. flash_attn_varlen_func(Q,K,V) → 3. index_copy_ 将K,V写入cache。K/V来自当前投影产出,非从cache读取。",
"hard_rule": "prefill 的 flash_attn_varlen_func 必须使用投影产出的 K,V (非从 KV cache 读取)。投影 K/V == cache K/V (RoPE 后数值相同),但从 cache 读取在 B>1 paged 场景下要求额外的 slot_mapping 反向查找——B=1 时二者等价但禁止从 cache 读取以保持接口一致性。"
},
"decode_path": {
"kernel": "flash_attn_with_kvcache_op(q, _key_cache, _value_cache, _kv_len_gpu, _block_table, softmax_scale, causal=False)",
"custom_op_registration": {
"desc": "kernel_replacement_plan.md (custom_op 注册) — @torch.library.custom_op('meta_infer::flash_attn_with_kvcache', mutates_args=()) + register_fake",
"complete_template": {
"registration": "@torch.library.custom_op('meta_infer::flash_attn_with_kvcache',mutates_args=())",
"impl": "def flash_attn_with_kvcache_op(q,k_cache,v_cache,cache_seqlens,block_table,softmax_scale,causal):\n # _fa_kvcache = flash_attn.flash_attn_interface.flash_attn_with_kvcache (import above)\n return _fa_kvcache(q,k_cache,v_cache,cache_seqlens=cache_seqlens,block_table=block_table,softmax_scale=softmax_scale,causal=causal)",
"fake": "@flash_attn_with_kvcache_op.register_fake\ndef _(q,k_cache,v_cache,cache_seqlens,block_table,softmax_scale,causal):\n return torch.empty_like(q)",
"dynamic_shape_note": "KV cache (k_cache/v_cache) 的 num_blocks 维度在推理期间固定 (prefill 时分配 max_blocks)。decode 扩容 block_table 不会改变 k_cache shape,仅 block_table 内容变化。因此 fake tensor shape 与实际运行时一致,不会触发重编译。若未来实现动态扩容,需使用 torch._dynamo.mark_dynamic(k_cache, 0) 告知 inductor。",
"import_statement": "from flash_attn.flash_attn_interface import flash_attn_with_kvcache as _fa_kvcache"
}
},
"q_format": "[1, 1, num_heads, head_dim] → reshape 为 [1, num_heads, head_dim]",
"softmax_scale": "1.0 / sqrt(head_dim) # 从 config.json 动态读取 head_dim。Qwen3-8B: 1/sqrt(128)≈0.08839"
},
"attempted_alternatives": {
"方案A_切片KV_SDPA无mask": "-20% 回退 — torch.compile 动态 shape 重编译",
"方案B_切片KV_SDPA去compile": "-12% 回退 — 切片开销 > 消除 attn_mask 收益",
"方案C_Vpadding_flash_attn": "-15% 回退 — F.pad/unpad 开销 + full buffer 浪费"
},
"failure_modes": [
"FM-013: flash_attn pybind11 扩展无法 torch.compile trace → 必须注册为 custom_op"
]
},
"tp_layer_interface_contracts": {
"tp_distributed_runtime": {
"source_impl": [
"engine/tp_layers/distributed.py"
],
"rank_size_contract": {
"tp_rank": "dist.get_rank() or env RANK",
"tp_size": "dist.get_world_size() or env WORLD_SIZE"
},
"collectives": {
"all_reduce_sum": {
"registration": "@torch.library.custom_op('meta_infer::all_reduce_sum', mutates_args=())",
"pseudocode": [
"def all_reduce_sum(x):",
" if not is_tp_enabled(): return x.clone() # tp_size=1, no-op, must return new tensor",
" if _custom_ar_handle is not None:",
" return _custom_ar_handle.all_reduce(x, registered=False) # CustomAR P2P staging buffer",
" y = x.clone()",
" dist.all_reduce(y, op=dist.ReduceOp.SUM) # NCCL fallback",
" return y",
"@all_reduce_sum.register_fake",
"def _(x): return torch.empty_like(x)"
],
"note": "custom_op 注册使 torch.compile 视其为黑盒原子节点,不追踪内部 (Dynamo guard immune)。TP=1 时返回 x.clone() (非 x 自身) — custom_op 禁止输出别名输入。",
"_ncc_fallback_contract": "⚠️ CustomAR→NCCL 回退是硬性生存要求。init_custom_ar 的 try/except 保证失败后 _custom_ar_handle=None(见 custom_ar_all_reduce.init_state_machine._failure_fallback_contract)。本函数的 if _custom_ar_handle is not None ... else dist.all_reduce 是自动生效的——不需要额外配置。Agent 实现时两个函数必须配合:init 置 None + all_reduce 判 None。"
},
"all_gather_last_dim": {
"signature": "def all_gather_last_dim(x: Tensor) -> Tensor",
"pseudocode": [
"if not is_tp_enabled(): return x",
"outs = [torch.empty_like(x) for _ in range(get_tp_size())]",
"dist.all_gather(outs, x)",
"return torch.cat(outs, dim=-1)"
],
"note": "使用 dist.all_gather (非 all_gather_into_tensor)。输入 [..., local_dim] → 输出 [..., local_dim * tp_size]。"
}
},
"_deprecated_artifact_note": "model_runner.py(已废弃)使用旧模块 tp_distributed,仅用于 TDD 测试。生产代码见 kernel_replacement_plan.md §九 (CustomAR)。",
"init_sequence": [
"1. torchrun: env LOCAL_RANK=0..N-1, RANK=0..N-1, WORLD_SIZE=N",
"2. torch.cuda.set_device(int(os.environ['LOCAL_RANK'])) # 每进程",
"3. dist.init_process_group(backend='nccl', init_method='env://') # LLMEngine 或 runner __init__",
"4. 模型 load_weights 后: init_custom_ar(device) — gloo secondary group + IPC exchange",
"5. dist.barrier() # 所有 rank 同步后进入 forward",
"device: 手动 .to(device),不依赖 torch.set_default_device"
]
},
"tp_embedding_and_lm_head": {
"source_impl": [
"engine/tp_layers/embedding.py"
],
"vocab_parallel_embedding": {
"input_ids": "[B, T] int64",
"local_weight": "[vocab_size/tp, hidden_size]",
"local_embedding": "[B, T, hidden_size] (masked local vocab)",
"output_after_all_reduce": "[B, T, hidden_size]",
"forward_pseudocode": [
"def forward(self, input_ids): # [B,T] int64",
" mask = (input_ids >= self.vocab_start) & (input_ids < self.vocab_end)",
" local_ids = (input_ids - self.vocab_start).masked_fill(~mask, 0)",
" out = F.embedding(local_ids, self.weight) # [B,T,embedding_dim]",
" out = out.masked_fill((~mask).unsqueeze(-1), 0)",
" return all_reduce_sum(out) # CustomAR P2P or NCCL fallback",
"# vocab_start = tp_rank * (vocab_size // tp_size); vocab_end = vocab_start + local_vocab_size"
]
},
"parallel_lm_head": {
"input_hidden": "[B, T, hidden_size]",
"local_logits": "[B, T, vocab_size/tp]",
"output_logits_gather": "[B, T, vocab_size]",
"forward_pseudocode": [
"def forward(self, hidden_states): # [B, T, hidden_size]",
" local_logits = F.linear(hidden_states, self.weight) # [B, T, vocab_size/tp]",
" # all_gather along last dim to get full vocab",
" logits = all_gather_last_dim(local_logits) # [B, T, vocab_size]",
" return logits"
]
}
},
"tp_linear_layers": {
"source_impl": [
"engine/tp_layers/linear.py"
],
"column_parallel_linear": {
"weight_shape": "[out/tp, in]",
"input": "[B, T, in]",
"output_no_gather": "[B, T, out/tp]",
"output_with_gather": "[B, T, out]"
},
"row_parallel_linear": {
"weight_shape": "[out, in/tp]",
"input": "[B, T, in/tp]",
"partial_output": "[B, T, out]",
"output_after_all_reduce": "[B, T, out]"
},
"qkv_column_parallel_forward": [
"# QKVColumnParallelLinear.forward():",
"def forward(self, x): # x: [B, T, hidden_size] e.g. [1,1,4096]",
" y = F.linear(x, self.weight) # [1,1, q_size+2*kv_size] = [1,1,1536]",
" if self.gather_output and self.tp_size > 1:",
" y = all_gather_last_dim(y)",
" q, k, v = y.split([self.q_size, self.kv_size, self.kv_size], dim=-1)",
" return q, k, v # q:[1,1,1024], k:[1,1,256], v:[1,1,256]",
"",
"# Caller-side reshape (in QwenAttentionTP.forward):",
"# q = q.view(B, T, self.num_heads, self.head_dim) # [1,1,8,128] — use num_heads=32/4=8",
"# k = k.view(B, T, self.num_kv_heads, self.head_dim) # [1,1,2,128] — use num_kv_heads_local=max(1,8//4)=2, NOT num_heads",
"# v = v.view(B, T, self.num_kv_heads, self.head_dim) # [1,1,2,128]",
"# WARNING: K/V reshape MUST use self.num_kv_heads (per-rank local), NOT self.num_heads.",
"# For Qwen3-8B TP=4: num_kv_heads_local = max(1, 8//4) = 2. Head dim = 128. 2*128=256 = kv_size. Correct.",
"# If mistakenly using num_heads=8: 8*128=1024 != kv_size=256 → RuntimeError shape mismatch. Safe.",
"",
"# MergedColumnParallelLinear.forward():",
"def forward(self, x):",
" y = F.linear(x, self.weight) # [B, T, 2*intermediate/tp] e.g. [1,1,6400]",
" return y # first half=gate, second half=up (for silu_and_mul input)"
],
"row_parallel_linear_forward": [
"def forward(self, x): # x: [B,T,in/tp]",
" y = F.linear(x, self.weight, None) # [B,T,out]",
" y = all_reduce_sum(y) # CustomAR P2P or NCCL fallback (always called, even tp_size=1 → no-op)",
" if self.bias is not None: y = y + self.bias",
" return y"
]
},
"qwen3_tp_model_interfaces": {
"source_impl": [
"engine/models/qwen.py"
],
"decoder_input_hidden": "[B, T, hidden_size]",
"qkv_projection": {
"_note": "实际使用 QKVColumnParallelLinear 合并投影(单次 GEMM → split → Q/K/V),非 3 个独立 ColumnParallelLinear",
"qkv_merged_weight": "[q_size + 2*kv_size, hidden_size] per rank (前半 Q,中 K,后 V)",
"qkv_output": "tuple(q:[B,T,num_heads/tp,head_dim], k:[B,T,num_kv_heads_local,head_dim], v:[B,T,num_kv_heads_local,head_dim])"
},
"attention": {
"_note": "实际使用 paged KV cache + flash_attn_with_kvcache;kv_len_gpu GPU scalar 替代 Python int",
"kv_cache_format": "paged: _key_cache[num_blocks, 256, num_kv_heads, head_dim], _value_cache 同理",
"block_table": "[1, max_blocks] int32, 固定 shape",
"decode_attention": "flash_attn_with_kvcache(q, _key_cache, _value_cache, _kv_len_gpu, _block_table, scale, causal=False)",
"prefill_kv_len_semantics": "prefill前_kv_len_gpu=0; 写入后=prefill_token_count。flash_attn_varlen_func使用cu_seqlens_k标记有效长度,不使用_kv_len_gpu。"
},
"mlp": {
"_note": "实际使用 MergedColumnParallelLinear 合并 gate+up(单次 GEMM),非 2 个独立 ColumnParallelLinear",
"gate_up_merged": "[B, T, 2*intermediate_size/tp] → silu_and_mul → [B, T, intermediate_size/tp]",
"down_out": "[B, T, hidden_size]"
},
"decode_forward_pattern": {
"_note": "decode 路径统一走 forward_decode(无控制流,torch.compile 兼容);CUDA_GRAPH=1 时走 forward_decode_graph(含 clone)",
"entry": "QwenForCausalLMTP.forward() 中 is_decode 分支调用 layer.forward_decode()",
"kv_len_tracking": "_kv_len_gpu GPU tensor(非 Python int),decode 步 forward 后 batch 读取 .item()",
"unified_signature": {
"function": "layer.forward_decode(hidden_states,positions,kv_len,max_seq_len,residual=None)",
"hidden_states": "[1,1,hidden_size]",
"positions": "[1] int64 = kv_len",
"kv_len": "int — 当前 KV 长度",
"max_seq_len": "int — max_model_len",
"residual": "Tensor|None — 首次为 None",
"return": "(hidden_states,residual)"
},
"kv_len_timing": {
"per_layer": "各层独立 _kv_len_gpu[1] GPU tensor,decode 步开始时各层值相同",
"write": "层内: slot=_kv_len_gpu[0]; index_copy_; _kv_len_gpu[0]+=1",
"read": "所有层完成后 batch 读取 .item() — 必须在非编译 forward() 中",
"hard_rule": ".item() 严禁在 forward_decode(编译函数)内执行"
},
"full_method_body": [
"# === QwenAttentionTP.forward_decode() — 完整 decode 热路径 ===",
"def forward_decode(self, hidden_states, positions, kv_len, max_seq_len):",
" B, S, H = hidden_states.shape # B=1, S=1",
" # 1. QKV projection + split",
" q, k, v = self.qkv_proj(hidden_states) # q:[1,1,q_size], k:[1,1,kv_size], v:[1,1,kv_size]",
" q = q.view(B, S, self.num_heads, self.head_dim) # [1,1,8,128]",
" k = k.view(B, S, self.num_kv_heads, self.head_dim) # [1,1,2,128]",
" v = v.view(B, S, self.num_kv_heads, self.head_dim)",
" # 2. Q/K norm",
" q = self.q_norm(q); k = self.k_norm(k)",
" # 3. RoPE (flatten to 2D)",
" q_flat = q.reshape(S, self.num_heads, self.head_dim)",
" k_flat = k.reshape(S, self.num_kv_heads, self.head_dim)",
" rotary_embedding(positions, q_flat, k_flat, self.head_dim, self._cos_sin_cache_gpu, is_neox=True)",
" q = q_flat.view(B, S, self.num_heads, self.head_dim)",
" k = k_flat.view(B, S, self.num_kv_heads, self.head_dim)",
" # 4. KV cache write (decode: write 1 token to slot=kv_len)",
" self._slot_mapping_decode[0] = self._kv_len_gpu[0]",
" k_write = k.reshape(1, self.num_kv_heads, self.head_dim)",
" v_write = v.reshape(1, self.num_kv_heads, self.head_dim)",
" kc_flat = self._key_cache.view(-1, self.num_kv_heads, self.head_dim)",
" vc_flat = self._value_cache.view(-1, self.num_kv_heads, self.head_dim)",
" kc_flat.index_copy_(0, self._slot_mapping_decode, k_write)",
" vc_flat.index_copy_(0, self._slot_mapping_decode, v_write)",
" self._kv_len_gpu[0] += 1",
" # 5. flash_attn_with_kvcache (read KV from paged cache)",
" q_attn = q.reshape(1, 1, self.num_heads, self.head_dim)",
" out = flash_attn_with_kvcache_op(q_attn, self._key_cache, self._value_cache, self._kv_len_gpu, self._block_table, self.scaling, causal=False)",
" # 6. o_proj + all_reduce",
" out = out.reshape(B, S, self.q_size)",
" return self.o_proj(out) # RowParallelLinear internally calls all_reduce_sum",
"",
"# === QwenDecoderLayerTP.forward_decode() ===",
"def forward_decode(self, hidden_states, positions, kv_len, max_seq_len, residual=None):",
" hs, res = hidden_states, residual # (eager path: no clone; graph path: clone first)",
" if res is None:",
" res = hs.clone(); rms_norm(hs, res, self.input_layernorm.weight, self.input_layernorm.eps) # first layer only",
" else:",
" fused_add_rms_norm(hs, res, self.input_layernorm.weight, self.input_layernorm.eps) # res+=hs; hs=rms_norm(res)",
" hs = self.self_attn.forward_decode(hs, positions, kv_len, max_seq_len) # attention (see above)",