HYHPING2023 commited on
Commit
edfa72c
·
verified ·
1 Parent(s): 37403bc

DFlash2 draft (epoch_3_step_35300) for Qwen3.5-35B-A3B VCLR3 target: 5-layer draft + two-tap dynamic convs + top-16 candidate selector, block 8

Browse files
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ tokenizer.json filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -1,3 +1,63 @@
1
  ---
2
  license: apache-2.0
 
 
 
 
 
 
 
 
3
  ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  license: apache-2.0
3
+ pipeline_tag: text-generation
4
+ library_name: transformers
5
+ inference: false
6
+ tags:
7
+ - dflash2
8
+ - speculative-decoding
9
+ - draft-model
10
+ - sglang
11
  ---
12
+
13
+ # checkpoint-draft-dflash2 (Qwen3.5-35B-A3B VCLR3 drafter)
14
+
15
+ DFlash2 speculative-decoding **draft model** for the Qwen3.5-35B-A3B (VCLR3 game-video
16
+ SFT) target. Not a standalone language model: it runs inside a speculative decoding
17
+ server and drafts tokens for the target to verify (target weights NOT included;
18
+ point `--model-path` at your own merged Qwen3.5-35B-A3B VCLR3 checkpoint).
19
+
20
+ ## Architecture
21
+
22
+ DFlash2 (Inco AI, [blog](https://inco.ai/blog/dflash2/); z-lab-compatible weight
23
+ layout) with block_size 8:
24
+
25
+ - 5-layer Qwen3-style dual-stream draft over the target's captured layer
26
+ `{1, 10, 19, 28, 37}` hidden states, full 248k vocab (uses the target's
27
+ embed_tokens / lm_head)
28
+ - Two-tap grouped dynamic causal convolutions (kernel 2, group 16) around every
29
+ attention/MLP sublayer — fixes block-end (suffix) decay
30
+ - Top-16 candidate path selector (rank 256): `S_t(a,b) = U_t(b) + <A(a) ⊙ H(h_t), B(b)>`
31
+
32
+ ## Serving (SGLang main, DFlash2-aware DFLASH worker)
33
+
34
+ ```bash
35
+ python -m sglang.launch_server \
36
+ --model-path /path/to/Qwen3.5-35B-A3B-vclr3-target \
37
+ --speculative-algorithm DFLASH \
38
+ --speculative-draft-model-path HYHPING2023/checkpoint-draft-dflash2 \
39
+ --speculative-num-draft-tokens 8 \
40
+ --trust-remote-code --tp 1 --enable-metrics
41
+ ```
42
+
43
+ Notes: request `chat_template_kwargs: {"enable_thinking": false}` (the draft was
44
+ trained on non-thinking answers); SGLang builds before DFlash2 support silently
45
+ ignore the selector.
46
+
47
+ ## Evaluation (temperature 0, greedy; block 8)
48
+
49
+ | Metric | DFlash1 baseline | **DFlash2 (this)** |
50
+ |---|---|---|
51
+ | Offline accept length / cycle (12.1k blocks, mixed corpus) | 2.72 | **2.97** (+9%) |
52
+ | — video blocks | — | **3.54** |
53
+ | — text blocks | — | 2.89 |
54
+ | SGLang serve accept length (single stream, text) | 1.80 | **2.15** (+19%) |
55
+
56
+ Per-position conditional acceptance rises 0.63 → 0.72 toward the block end
57
+ (DFlash1 stays flat ~0.60) — the convolution's suffix-decay fix.
58
+
59
+ ## Training
60
+
61
+ 4 epochs on the mixed video+zh+en corpus (70.6k samples), warm-started from the
62
+ DFlash1 b8 mixed2 checkpoint, FSDP + frozen online 35B target. Teacher-forced
63
+ selector CE. Checkpoint: `epoch_3_step_35300`.
chat_template.jinja ADDED
@@ -0,0 +1,154 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {%- set image_count = namespace(value=0) %}
2
+ {%- set video_count = namespace(value=0) %}
3
+ {%- macro render_content(content, do_vision_count, is_system_content=false) %}
4
+ {%- if content is string %}
5
+ {{- content }}
6
+ {%- elif content is iterable and content is not mapping %}
7
+ {%- for item in content %}
8
+ {%- if 'image' in item or 'image_url' in item or item.type == 'image' %}
9
+ {%- if is_system_content %}
10
+ {{- raise_exception('System message cannot contain images.') }}
11
+ {%- endif %}
12
+ {%- if do_vision_count %}
13
+ {%- set image_count.value = image_count.value + 1 %}
14
+ {%- endif %}
15
+ {%- if add_vision_id %}
16
+ {{- 'Picture ' ~ image_count.value ~ ': ' }}
17
+ {%- endif %}
18
+ {{- '<|vision_start|><|image_pad|><|vision_end|>' }}
19
+ {%- elif 'video' in item or item.type == 'video' %}
20
+ {%- if is_system_content %}
21
+ {{- raise_exception('System message cannot contain videos.') }}
22
+ {%- endif %}
23
+ {%- if do_vision_count %}
24
+ {%- set video_count.value = video_count.value + 1 %}
25
+ {%- endif %}
26
+ {%- if add_vision_id %}
27
+ {{- 'Video ' ~ video_count.value ~ ': ' }}
28
+ {%- endif %}
29
+ {{- '<|vision_start|><|video_pad|><|vision_end|>' }}
30
+ {%- elif 'text' in item %}
31
+ {{- item.text }}
32
+ {%- else %}
33
+ {{- raise_exception('Unexpected item type in content.') }}
34
+ {%- endif %}
35
+ {%- endfor %}
36
+ {%- elif content is none or content is undefined %}
37
+ {{- '' }}
38
+ {%- else %}
39
+ {{- raise_exception('Unexpected content type.') }}
40
+ {%- endif %}
41
+ {%- endmacro %}
42
+ {%- if not messages %}
43
+ {{- raise_exception('No messages provided.') }}
44
+ {%- endif %}
45
+ {%- if tools and tools is iterable and tools is not mapping %}
46
+ {{- '<|im_start|>system\n' }}
47
+ {{- "# Tools\n\nYou have access to the following functions:\n\n<tools>" }}
48
+ {%- for tool in tools %}
49
+ {{- "\n" }}
50
+ {{- tool | tojson }}
51
+ {%- endfor %}
52
+ {{- "\n</tools>" }}
53
+ {{- '\n\nIf you choose to call a function ONLY reply in the following format with NO suffix:\n\n<tool_call>\n<function=example_function_name>\n<parameter=example_parameter_1>\nvalue_1\n</parameter>\n<parameter=example_parameter_2>\nThis is the value for the second parameter\nthat can span\nmultiple lines\n</parameter>\n</function>\n</tool_call>\n\n<IMPORTANT>\nReminder:\n- Function calls MUST follow the specified format: an inner <function=...></function> block must be nested within <tool_call></tool_call> XML tags\n- Required parameters MUST be specified\n- You may provide optional reasoning for your function call in natural language BEFORE the function call, but NOT after\n- If there is no function call available, answer the question like normal with your current knowledge and do not tell the user about function calls\n</IMPORTANT>' }}
54
+ {%- if messages[0].role == 'system' %}
55
+ {%- set content = render_content(messages[0].content, false, true)|trim %}
56
+ {%- if content %}
57
+ {{- '\n\n' + content }}
58
+ {%- endif %}
59
+ {%- endif %}
60
+ {{- '<|im_end|>\n' }}
61
+ {%- else %}
62
+ {%- if messages[0].role == 'system' %}
63
+ {%- set content = render_content(messages[0].content, false, true)|trim %}
64
+ {{- '<|im_start|>system\n' + content + '<|im_end|>\n' }}
65
+ {%- endif %}
66
+ {%- endif %}
67
+ {%- set ns = namespace(multi_step_tool=true, last_query_index=messages|length - 1) %}
68
+ {%- for message in messages[::-1] %}
69
+ {%- set index = (messages|length - 1) - loop.index0 %}
70
+ {%- if ns.multi_step_tool and message.role == "user" %}
71
+ {%- set content = render_content(message.content, false)|trim %}
72
+ {%- if not(content.startswith('<tool_response>') and content.endswith('</tool_response>')) %}
73
+ {%- set ns.multi_step_tool = false %}
74
+ {%- set ns.last_query_index = index %}
75
+ {%- endif %}
76
+ {%- endif %}
77
+ {%- endfor %}
78
+ {%- if ns.multi_step_tool %}
79
+ {{- raise_exception('No user query found in messages.') }}
80
+ {%- endif %}
81
+ {%- for message in messages %}
82
+ {%- set content = render_content(message.content, true)|trim %}
83
+ {%- if message.role == "system" %}
84
+ {%- if not loop.first %}
85
+ {{- raise_exception('System message must be at the beginning.') }}
86
+ {%- endif %}
87
+ {%- elif message.role == "user" %}
88
+ {{- '<|im_start|>' + message.role + '\n' + content + '<|im_end|>' + '\n' }}
89
+ {%- elif message.role == "assistant" %}
90
+ {%- set reasoning_content = '' %}
91
+ {%- if message.reasoning_content is string %}
92
+ {%- set reasoning_content = message.reasoning_content %}
93
+ {%- else %}
94
+ {%- if '</think>' in content %}
95
+ {%- set reasoning_content = content.split('</think>')[0].rstrip('\n').split('<think>')[-1].lstrip('\n') %}
96
+ {%- set content = content.split('</think>')[-1].lstrip('\n') %}
97
+ {%- endif %}
98
+ {%- endif %}
99
+ {%- set reasoning_content = reasoning_content|trim %}
100
+ {%- if loop.index0 > ns.last_query_index %}
101
+ {{- '<|im_start|>' + message.role + '\n<think>\n' + reasoning_content + '\n</think>\n\n' + content }}
102
+ {%- else %}
103
+ {{- '<|im_start|>' + message.role + '\n' + content }}
104
+ {%- endif %}
105
+ {%- if message.tool_calls and message.tool_calls is iterable and message.tool_calls is not mapping %}
106
+ {%- for tool_call in message.tool_calls %}
107
+ {%- if tool_call.function is defined %}
108
+ {%- set tool_call = tool_call.function %}
109
+ {%- endif %}
110
+ {%- if loop.first %}
111
+ {%- if content|trim %}
112
+ {{- '\n\n<tool_call>\n<function=' + tool_call.name + '>\n' }}
113
+ {%- else %}
114
+ {{- '<tool_call>\n<function=' + tool_call.name + '>\n' }}
115
+ {%- endif %}
116
+ {%- else %}
117
+ {{- '\n<tool_call>\n<function=' + tool_call.name + '>\n' }}
118
+ {%- endif %}
119
+ {%- if tool_call.arguments is defined %}
120
+ {%- for args_name, args_value in tool_call.arguments|items %}
121
+ {{- '<parameter=' + args_name + '>\n' }}
122
+ {%- set args_value = args_value | tojson | safe if args_value is mapping or (args_value is sequence and args_value is not string) else args_value | string %}
123
+ {{- args_value }}
124
+ {{- '\n</parameter>\n' }}
125
+ {%- endfor %}
126
+ {%- endif %}
127
+ {{- '</function>\n</tool_call>' }}
128
+ {%- endfor %}
129
+ {%- endif %}
130
+ {{- '<|im_end|>\n' }}
131
+ {%- elif message.role == "tool" %}
132
+ {%- if loop.previtem and loop.previtem.role != "tool" %}
133
+ {{- '<|im_start|>user' }}
134
+ {%- endif %}
135
+ {{- '\n<tool_response>\n' }}
136
+ {{- content }}
137
+ {{- '\n</tool_response>' }}
138
+ {%- if not loop.last and loop.nextitem.role != "tool" %}
139
+ {{- '<|im_end|>\n' }}
140
+ {%- elif loop.last %}
141
+ {{- '<|im_end|>\n' }}
142
+ {%- endif %}
143
+ {%- else %}
144
+ {{- raise_exception('Unexpected message role.') }}
145
+ {%- endif %}
146
+ {%- endfor %}
147
+ {%- if add_generation_prompt %}
148
+ {{- '<|im_start|>assistant\n' }}
149
+ {%- if enable_thinking is defined and enable_thinking is false %}
150
+ {{- '<think>\n\n</think>\n\n' }}
151
+ {%- else %}
152
+ {{- '<think>\n' }}
153
+ {%- endif %}
154
+ {%- endif %}
config.json ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "transformers_version": "5.8.1",
3
+ "architectures": [
4
+ "DFlash2DraftModel"
5
+ ],
6
+ "output_hidden_states": false,
7
+ "return_dict": true,
8
+ "dtype": "bfloat16",
9
+ "chunk_size_feed_forward": 0,
10
+ "is_encoder_decoder": false,
11
+ "id2label": {
12
+ "0": "LABEL_0",
13
+ "1": "LABEL_1"
14
+ },
15
+ "label2id": {
16
+ "LABEL_0": 0,
17
+ "LABEL_1": 1
18
+ },
19
+ "problem_type": null,
20
+ "vocab_size": 248320,
21
+ "hidden_size": 2048,
22
+ "intermediate_size": 8192,
23
+ "num_hidden_layers": 5,
24
+ "num_attention_heads": 16,
25
+ "num_key_value_heads": 2,
26
+ "head_dim": 256,
27
+ "hidden_act": "silu",
28
+ "max_position_embeddings": 262144,
29
+ "initializer_range": 0.02,
30
+ "rms_norm_eps": 1e-06,
31
+ "use_cache": true,
32
+ "tie_word_embeddings": false,
33
+ "rope_parameters": {
34
+ "rope_theta": 1000000,
35
+ "rope_type": "default"
36
+ },
37
+ "attention_bias": false,
38
+ "use_sliding_window": false,
39
+ "sliding_window": null,
40
+ "max_window_layers": 5,
41
+ "layer_types": [
42
+ "full_attention",
43
+ "full_attention",
44
+ "full_attention",
45
+ "full_attention",
46
+ "full_attention"
47
+ ],
48
+ "attention_dropout": 0.0,
49
+ "pad_token_id": null,
50
+ "bos_token_id": null,
51
+ "eos_token_id": 248044,
52
+ "_name_or_path": "output/dflash2/draft_config.json",
53
+ "model_type": "qwen3",
54
+ "auto_map": {
55
+ "AutoModel": "dflash.DFlash2DraftModel"
56
+ },
57
+ "block_size": 8,
58
+ "dflash_config": {
59
+ "mask_token_id": 248043,
60
+ "target_layer_ids": [
61
+ 1,
62
+ 10,
63
+ 19,
64
+ 28,
65
+ 37
66
+ ],
67
+ "conv_kernel_size": 2,
68
+ "conv_group_size": 16,
69
+ "selector_rank": 256,
70
+ "selector_top_k": 16
71
+ },
72
+ "num_target_layers": 40,
73
+ "output_attentions": false
74
+ }
dflash.py ADDED
@@ -0,0 +1,908 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding=utf-8
2
+ # DFlash2 draft model — self-contained (single file) on purpose.
3
+ #
4
+ # Sources, cross-verified against each other:
5
+ # 1. drafttrain/dflash/_vendored/dflash_model.py — the SpecForge DFlash1 backbone this
6
+ # repo already trains and serves through SGLang's DFLASH algorithm. The backbone
7
+ # code below is copied VERBATIM from it (attention / decoder layer / model /
8
+ # spec_generate) so train<->serve behavior matches the proven DFlash1 path.
9
+ # 2. z-lab/dflash dflash/model.py (Apache-2.0) — the official DFlash2DraftModel
10
+ # reference: GroupedDynamicCausalConv (conv_kernel_size=2, conv_group_size=16) and
11
+ # CandidateSelector (selector_rank=256, selector_top_k=16). Parameter names are
12
+ # kept identical (layers.{i}.attention_conv/mlp_conv.{base_kernel,kernel_projection},
13
+ # candidate_selector.{predecessor_codebook,successor_codebook,hidden_projection})
14
+ # so exported weights load under SGLang's DFlash2 support and under the z-lab
15
+ # reference implementation unchanged.
16
+ #
17
+ # Two deliberate deviations from z-lab's reference:
18
+ # - The conv is BLOCK-LOCAL during training: when the noise stream is a whole number
19
+ # of blocks (the training layout: N blocks of [anchor, mask*bs-1] concatenated),
20
+ # the predecessor tap zero-pads at each block start instead of reading across
21
+ # blocks — exactly matching inference, where the draft sees one block at a time.
22
+ # With a single block (<= block_size, the inference shape) the behavior is
23
+ # bit-identical to z-lab's F.pad reference.
24
+ # - conv_identity_init (default True): base kernel starts as [1, 0] (identity) and
25
+ # kernel_projection at zero, so a warm-started DFlash1 checkpoint behaves
26
+ # identically at step 0 and the convs grow in smoothly during training.
27
+ #
28
+ # This file must stay importable stand-alone: export_draft.py copies it into the served
29
+ # draft directory as ``dflash.py`` (auto_map -> "dflash.DFlash2DraftModel"), where no
30
+ # ``drafttrain`` package exists.
31
+
32
+ from typing import Callable, ClassVar, Optional
33
+
34
+ import torch
35
+ import torch.nn.functional as F
36
+ from torch import nn
37
+ from transformers import DynamicCache
38
+ from transformers.cache_utils import Cache
39
+ from transformers.modeling_outputs import CausalLMOutputWithPast
40
+ from transformers.models.qwen3.modeling_qwen3 import (
41
+ ALL_ATTENTION_FUNCTIONS,
42
+ FlashAttentionKwargs,
43
+ GradientCheckpointingLayer,
44
+ Qwen3Config,
45
+ Qwen3MLP,
46
+ Qwen3PreTrainedModel,
47
+ Qwen3RMSNorm,
48
+ Qwen3RotaryEmbedding,
49
+ eager_attention_forward,
50
+ rotate_half,
51
+ )
52
+ from typing_extensions import Tuple, Unpack
53
+
54
+
55
+ def sample(logits: torch.Tensor, temperature: float = 0.0) -> torch.Tensor:
56
+ if temperature < 1e-5:
57
+ return torch.argmax(logits, dim=-1)
58
+ bsz, seq_len, vocab_size = logits.shape
59
+ logits = logits.view(-1, vocab_size)
60
+ logits = logits / temperature
61
+ probs = torch.softmax(logits, dim=-1)
62
+ return torch.multinomial(probs, num_samples=1).view(bsz, seq_len)
63
+
64
+
65
+ def _sampling_probs(
66
+ logits: torch.Tensor,
67
+ temperature: float,
68
+ top_p: float = 1.0,
69
+ top_k: int = 0,
70
+ ) -> torch.Tensor:
71
+ """Softmax over (optionally top-k/top-p filtered) logits, scattered back to full vocab."""
72
+ scores = logits.float() / temperature
73
+ vocab_size = scores.shape[-1]
74
+ if 0 < top_k < vocab_size:
75
+ scores, indices = torch.topk(scores, top_k, dim=-1)
76
+ else:
77
+ indices = None
78
+
79
+ probs = torch.softmax(scores, dim=-1)
80
+ if top_p < 1.0:
81
+ sorted_probs, order = probs.sort(dim=-1, descending=True)
82
+ keep = sorted_probs.cumsum(dim=-1) - sorted_probs < top_p
83
+ sorted_probs = sorted_probs * keep
84
+ probs = torch.zeros_like(probs).scatter(-1, order, sorted_probs)
85
+ probs = probs / probs.sum(dim=-1, keepdim=True)
86
+
87
+ if indices is not None:
88
+ probs = torch.zeros_like(logits, dtype=probs.dtype).scatter(-1, indices, probs)
89
+ return probs
90
+
91
+
92
+ def _sample_probs(probs: torch.Tensor) -> torch.Tensor:
93
+ shape = probs.shape[:-1]
94
+ return torch.multinomial(probs.view(-1, probs.shape[-1]), 1).view(shape)
95
+
96
+
97
+ def apply_rotary_pos_emb(q, k, cos, sin, position_ids=None, unsqueeze_dim=1):
98
+ cos = cos.unsqueeze(unsqueeze_dim)
99
+ sin = sin.unsqueeze(unsqueeze_dim)
100
+ q_len = q.size(-2)
101
+ q_embed = (q * cos[..., -q_len:, :]) + (rotate_half(q) * sin[..., -q_len:, :])
102
+ k_embed = (k * cos) + (rotate_half(k) * sin)
103
+ return q_embed, k_embed
104
+
105
+
106
+ def _dflash_config(config) -> dict:
107
+ return getattr(config, "dflash_config", {}) or {}
108
+
109
+
110
+ def _draft_value(config, name, default=None):
111
+ return _dflash_config(config).get(name, getattr(config, name, default))
112
+
113
+
114
+ class Qwen3DFlashAttention(nn.Module):
115
+ """Dual-stream attention (copied from the vendored DFlash1 model, unchanged).
116
+
117
+ K/V are computed from BOTH the captured target hidden states (context stream) and
118
+ the draft's own noise-stream hidden states, concatenated.
119
+ """
120
+
121
+ def __init__(self, config: Qwen3Config, layer_idx: int):
122
+ super().__init__()
123
+ self.config = config
124
+ self.layer_idx = layer_idx
125
+ self.head_dim = getattr(
126
+ config, "head_dim", config.hidden_size // config.num_attention_heads
127
+ )
128
+ self.num_key_value_groups = (
129
+ config.num_attention_heads // config.num_key_value_heads
130
+ )
131
+ self.scaling = self.head_dim**-0.5
132
+ self.attention_dropout = config.attention_dropout
133
+ self.is_causal = False
134
+ self.q_proj = nn.Linear(
135
+ config.hidden_size,
136
+ config.num_attention_heads * self.head_dim,
137
+ bias=config.attention_bias,
138
+ )
139
+ self.k_proj = nn.Linear(
140
+ config.hidden_size,
141
+ config.num_key_value_heads * self.head_dim,
142
+ bias=config.attention_bias,
143
+ )
144
+ self.v_proj = nn.Linear(
145
+ config.hidden_size,
146
+ config.num_key_value_heads * self.head_dim,
147
+ bias=config.attention_bias,
148
+ )
149
+ self.o_proj = nn.Linear(
150
+ config.num_attention_heads * self.head_dim,
151
+ config.hidden_size,
152
+ bias=config.attention_bias,
153
+ )
154
+ self.q_norm = Qwen3RMSNorm(self.head_dim, eps=config.rms_norm_eps)
155
+ self.k_norm = Qwen3RMSNorm(self.head_dim, eps=config.rms_norm_eps)
156
+ self.sliding_window = (
157
+ config.sliding_window
158
+ if config.layer_types[layer_idx] == "sliding_attention"
159
+ else None
160
+ )
161
+
162
+ def forward(
163
+ self,
164
+ hidden_states: torch.Tensor,
165
+ target_hidden: torch.Tensor,
166
+ position_embeddings: tuple[torch.Tensor, torch.Tensor],
167
+ attention_mask: Optional[torch.Tensor],
168
+ past_key_values: Optional[Cache] = None,
169
+ cache_position: Optional[torch.LongTensor] = None,
170
+ **kwargs: Unpack[FlashAttentionKwargs],
171
+ ) -> tuple[torch.Tensor, Optional[torch.Tensor]]:
172
+ bsz, q_len = hidden_states.shape[:-1]
173
+ ctx_len = target_hidden.shape[1]
174
+ q = self.q_proj(hidden_states)
175
+ q = q.view(bsz, q_len, -1, self.head_dim)
176
+ q = self.q_norm(q).transpose(1, 2)
177
+ k_ctx = self.k_proj(target_hidden)
178
+ k_noise = self.k_proj(hidden_states)
179
+ v_ctx = self.v_proj(target_hidden)
180
+ v_noise = self.v_proj(hidden_states)
181
+ k = torch.cat([k_ctx, k_noise], dim=1).view(
182
+ bsz, ctx_len + q_len, -1, self.head_dim
183
+ )
184
+ v = torch.cat([v_ctx, v_noise], dim=1).view(
185
+ bsz, ctx_len + q_len, -1, self.head_dim
186
+ )
187
+ k = self.k_norm(k).transpose(1, 2)
188
+ v = v.transpose(1, 2)
189
+ cos, sin = position_embeddings
190
+ q, k = apply_rotary_pos_emb(q, k, cos, sin)
191
+ if past_key_values is not None:
192
+ cache_kwargs = {"sin": sin, "cos": cos, "cache_position": cache_position}
193
+ k, v = past_key_values.update(k, v, self.layer_idx, cache_kwargs)
194
+ attn_fn: Callable = eager_attention_forward
195
+ if self.config._attn_implementation != "eager":
196
+ attn_fn = ALL_ATTENTION_FUNCTIONS[self.config._attn_implementation]
197
+ attn_output, attn_weights = attn_fn(
198
+ self,
199
+ q,
200
+ k,
201
+ v,
202
+ attention_mask,
203
+ dropout=0.0 if not self.training else self.attention_dropout,
204
+ scaling=self.scaling,
205
+ sliding_window=self.sliding_window,
206
+ **kwargs,
207
+ )
208
+ attn_output = attn_output.reshape(bsz, q_len, -1)
209
+ attn_output = self.o_proj(attn_output)
210
+ return attn_output, attn_weights
211
+
212
+
213
+ # --------------------------------------------------------------------------- #
214
+ # DFlash2 addition 1: two-tap grouped dynamic causal convolution
215
+ # --------------------------------------------------------------------------- #
216
+
217
+ def _grouped_dynamic_convolve(hidden, dynamic, base, group_size):
218
+ """z-lab reference convolve: out[t] = sum_offset (base[offset] + dynamic[t, offset]) * x[t-offset].
219
+
220
+ ``hidden`` (B', L', H), ``dynamic`` (B', L', kernel_size, groups), ``base``
221
+ (kernel_size, H). Zero left-pad: x[t-offset] = 0 for t < offset.
222
+ """
223
+ batch, length, hidden_size = hidden.shape
224
+ groups = hidden_size // group_size
225
+ blocks = hidden.view(batch, length, groups, group_size)
226
+ dynamic = dynamic.view(batch, length, base.shape[0], groups, 1)
227
+ output = torch.zeros_like(blocks)
228
+ for offset in range(base.shape[0]):
229
+ values = blocks if offset == 0 else F.pad(blocks[:, :-offset], (0, 0, 0, 0, offset, 0))
230
+ kernel = base[offset].view(1, 1, groups, group_size).to(hidden.dtype)
231
+ output = output + kernel * values
232
+ output = torch.addcmul(output, dynamic[:, :, offset], values)
233
+ return output.view_as(hidden)
234
+
235
+
236
+ class GroupedDynamicCausalConv(nn.Module):
237
+ """Two-tap grouped dynamic causal convolution (DFlash2).
238
+
239
+ Parameter layout is identical to z-lab/dflash's GroupedDynamicCausalConv:
240
+ - ``base_kernel``: (2, kernel_size, hidden). Index 0 holds the static taps for
241
+ the PRE-sublayer conv (``prepare``), index 1 for the POST-sublayer conv
242
+ (``finish``). The two dynamic kernels are computed once from the pre-sublayer
243
+ (normed) hidden state and reused by both.
244
+ - ``kernel_projection``: Linear(hidden, 2 * kernel_size * groups, bias=False) —
245
+ per-position dynamic tap corrections; every ``group_size`` channels share one.
246
+
247
+ Block-local extension (training): when the input length is a whole number of
248
+ blocks AND longer than one block, the sequence is processed as concatenated
249
+ independent blocks — the predecessor tap zero-pads at each block start instead of
250
+ reading the previous block's tail. This exactly matches inference, where the draft
251
+ forward sees a single block ([anchor, mask, ...]) at a time. With a single block
252
+ (length <= block_size) the computation is bit-identical to the z-lab reference.
253
+ """
254
+
255
+ def __init__(self, hidden_size: int, kernel_size: int, group_size: int, block_size: int):
256
+ super().__init__()
257
+ if hidden_size % group_size != 0:
258
+ raise ValueError(
259
+ f"GroupedDynamicCausalConv requires group_size to divide hidden_size; "
260
+ f"got hidden_size={hidden_size}, group_size={group_size}"
261
+ )
262
+ if kernel_size < 1:
263
+ raise ValueError(f"kernel_size must be >= 1, got {kernel_size}")
264
+ if block_size < 1:
265
+ raise ValueError(f"block_size must be >= 1, got {block_size}")
266
+ self.kernel_size = kernel_size
267
+ self.group_size = group_size
268
+ self.block_size = block_size
269
+ self.num_groups = hidden_size // group_size
270
+ self.base_kernel = nn.Parameter(torch.empty(2, kernel_size, hidden_size))
271
+ self.kernel_projection = nn.Linear(
272
+ hidden_size, 2 * kernel_size * self.num_groups, bias=False
273
+ )
274
+
275
+ def _split_blocks(self, hidden: torch.Tensor):
276
+ """(B, L, ...) -> ((B*N, bs, ...), bsz, n). Whole-block sequences (L = N*bs > bs,
277
+ the training noise layout) become N independent blocks; anything else (a single
278
+ full or partial block, the inference layout) is returned as one block."""
279
+ bsz, seq_len = hidden.shape[0], hidden.shape[1]
280
+ if seq_len > self.block_size and seq_len % self.block_size == 0:
281
+ n = seq_len // self.block_size
282
+ return hidden.reshape(bsz * n, self.block_size, *hidden.shape[2:]), bsz, n
283
+ return hidden, bsz, 1
284
+
285
+ def prepare(self, hidden: torch.Tensor):
286
+ """Pre-sublayer: convolve the (normed) input; stash the finish-step taps.
287
+
288
+ Returns (conv_out (B, L, H), dynamic (B, L, kernel_size, groups)).
289
+ """
290
+ bsz, seq_len = hidden.shape[0], hidden.shape[1]
291
+ blocked, _, _ = self._split_blocks(hidden)
292
+ dynamic = self.kernel_projection(blocked).view(
293
+ *blocked.shape[:-1], 2, self.kernel_size, self.num_groups
294
+ )
295
+ out = _grouped_dynamic_convolve(
296
+ blocked, dynamic[..., 0, :, :], self.base_kernel[0], self.group_size
297
+ )
298
+ return (
299
+ out.reshape(bsz, seq_len, hidden.shape[-1]),
300
+ dynamic[..., 1, :, :].reshape(
301
+ bsz, seq_len, self.kernel_size, self.num_groups
302
+ ),
303
+ )
304
+
305
+ def finish(self, hidden: torch.Tensor, dynamic: torch.Tensor) -> torch.Tensor:
306
+ """Post-sublayer: convolve the sublayer output with the stashed taps."""
307
+ bsz, seq_len = hidden.shape[0], hidden.shape[1]
308
+ blocked, _, _ = self._split_blocks(hidden)
309
+ dyn_blocked = dynamic.reshape(
310
+ blocked.shape[0], blocked.shape[1], self.kernel_size, self.num_groups
311
+ )
312
+ out = _grouped_dynamic_convolve(
313
+ blocked, dyn_blocked, self.base_kernel[1], self.group_size
314
+ )
315
+ return out.reshape(bsz, seq_len, hidden.shape[-1])
316
+
317
+
318
+ # --------------------------------------------------------------------------- #
319
+ # Decoder layer (DFlash1 layer + optional DFlash2 conv hooks, z-lab pattern)
320
+ # --------------------------------------------------------------------------- #
321
+
322
+ class Qwen3DFlashDecoderLayer(GradientCheckpointingLayer):
323
+ """DFlash decoder layer with optional DFlash2 conv hooks.
324
+
325
+ With ``attention_conv``/``mlp_conv`` None (DFlash1) the forward is identical to
326
+ the vendored SpecForge layer. When set (DFlash2), each sublayer is wrapped as:
327
+ prepare(normed input) -> sublayer -> finish(sublayer output), exactly z-lab's
328
+ placement. The convs act ONLY on the noise stream (``hidden_states``); the context
329
+ stream (``target_hidden``) feeds k_ctx/v_ctx directly, unconverted.
330
+ """
331
+
332
+ def __init__(self, config: Qwen3Config, layer_idx: int):
333
+ super().__init__()
334
+ self.hidden_size = config.hidden_size
335
+ self.self_attn = Qwen3DFlashAttention(config=config, layer_idx=layer_idx)
336
+ self.mlp = Qwen3MLP(config)
337
+ self.input_layernorm = Qwen3RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
338
+ self.post_attention_layernorm = Qwen3RMSNorm(
339
+ config.hidden_size, eps=config.rms_norm_eps
340
+ )
341
+ self.attention_conv: Optional[GroupedDynamicCausalConv] = None
342
+ self.mlp_conv: Optional[GroupedDynamicCausalConv] = None
343
+
344
+ def forward(
345
+ self,
346
+ target_hidden: Optional[torch.Tensor] = None,
347
+ hidden_states: Optional[torch.Tensor] = None,
348
+ attention_mask: Optional[torch.Tensor] = None,
349
+ position_ids: Optional[torch.LongTensor] = None,
350
+ past_key_value: Optional[Cache] = None,
351
+ output_attentions: Optional[bool] = False,
352
+ use_cache: Optional[bool] = False,
353
+ cache_position: Optional[torch.LongTensor] = None,
354
+ position_embeddings: Optional[
355
+ Tuple[torch.Tensor, torch.Tensor]
356
+ ] = None, # necessary, but kept here for BC
357
+ **kwargs: Unpack[FlashAttentionKwargs],
358
+ ) -> Tuple[
359
+ torch.FloatTensor, Optional[Tuple[torch.FloatTensor, torch.FloatTensor]]
360
+ ]:
361
+ residual = hidden_states
362
+ hidden_states = self.input_layernorm(hidden_states)
363
+ attention_kernel = None
364
+ if self.attention_conv is not None:
365
+ hidden_states, attention_kernel = self.attention_conv.prepare(hidden_states)
366
+ hidden_states = self.self_attn(
367
+ hidden_states=hidden_states,
368
+ target_hidden=target_hidden,
369
+ attention_mask=attention_mask,
370
+ position_ids=position_ids,
371
+ past_key_value=past_key_value,
372
+ output_attentions=output_attentions,
373
+ use_cache=use_cache,
374
+ cache_position=cache_position,
375
+ position_embeddings=position_embeddings,
376
+ **kwargs,
377
+ )[0]
378
+ if attention_kernel is not None:
379
+ hidden_states = self.attention_conv.finish(hidden_states, attention_kernel)
380
+ hidden_states = residual + hidden_states
381
+ residual = hidden_states
382
+ hidden_states = self.post_attention_layernorm(hidden_states)
383
+ mlp_kernel = None
384
+ if self.mlp_conv is not None:
385
+ hidden_states, mlp_kernel = self.mlp_conv.prepare(hidden_states)
386
+ hidden_states = self.mlp(hidden_states)
387
+ if mlp_kernel is not None:
388
+ hidden_states = self.mlp_conv.finish(hidden_states, mlp_kernel)
389
+ hidden_states = residual + hidden_states
390
+ return hidden_states
391
+
392
+
393
+ def build_target_layer_ids(num_target_layers: int, num_draft_layers: int):
394
+ if num_draft_layers == 1:
395
+ return [(num_target_layers // 2)]
396
+ start = 1
397
+ end = num_target_layers - 3
398
+ span = end - start
399
+ target_layer_ids = [
400
+ int(round(start + (i * span) / (num_draft_layers - 1)))
401
+ for i in range(num_draft_layers)
402
+ ]
403
+ return target_layer_ids
404
+
405
+
406
+ def extract_context_feature(
407
+ hidden_states: list[torch.Tensor],
408
+ layer_ids: Optional[list[int]],
409
+ ) -> torch.Tensor:
410
+ offset = 1
411
+ selected_states = []
412
+ for layer_id in layer_ids:
413
+ selected_states.append(hidden_states[layer_id + offset])
414
+ target_hidden = torch.cat(selected_states, dim=-1)
415
+ return target_hidden
416
+
417
+
418
+ # --------------------------------------------------------------------------- #
419
+ # DFlash2 addition 2: candidate path selector
420
+ # --------------------------------------------------------------------------- #
421
+
422
+ class CandidateSelector(nn.Module):
423
+ """Top-k candidate path selector (z-lab-compatible parameter layout).
424
+
425
+ Scores adjacent candidate pairs with a gated low-rank bilinear form::
426
+
427
+ S_t(a, b) = U_t(b) + < A(a) ⊙ H(h_t), B(b) >
428
+
429
+ where ``U_t`` is the draft's own logit for candidate ``b`` (how much the drafter
430
+ liked it on its own), ``A``/``B`` are compact per-token codebooks and ``H(h_t)`` is
431
+ a context gate projected from the draft hidden state deciding which parts of the
432
+ predecessor/successor match count.
433
+ """
434
+
435
+ def __init__(self, config):
436
+ super().__init__()
437
+ rank = int(_draft_value(config, "selector_rank", 256))
438
+ top_k = int(_draft_value(config, "selector_top_k", 16))
439
+ if rank <= 0:
440
+ raise ValueError(f"selector_rank must be > 0, got {rank}")
441
+ if top_k <= 0:
442
+ raise ValueError(f"selector_top_k must be > 0, got {top_k}")
443
+ self.rank = rank
444
+ self.top_k = top_k
445
+ self.predecessor_codebook = nn.Embedding(config.vocab_size, rank)
446
+ self.successor_codebook = nn.Embedding(config.vocab_size, rank)
447
+ self.hidden_projection = nn.Linear(config.hidden_size, rank, bias=False)
448
+
449
+ def pairwise_scores(
450
+ self,
451
+ hidden: torch.Tensor,
452
+ logits: torch.Tensor,
453
+ prev_ids: torch.Tensor,
454
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
455
+ """Teacher-forced pairwise scores for TRAINING.
456
+
457
+ hidden (B, L, H), logits (B, L, V), prev_ids (B, L) true predecessor token ids.
458
+ Returns (candidates (B, L, k), scores (B, L, k)) with
459
+ ``scores[b, t, j] = logits[b, t, cand_j] + <A(prev) ⊙ H(h_t), B(cand_j)>``.
460
+ Fully parallel — no sequential walk (teacher forcing supplies the predecessor).
461
+ """
462
+ unary, candidates = torch.topk(logits, self.top_k, dim=-1, sorted=False)
463
+ gate = self.hidden_projection(hidden) # (B, L, R)
464
+ pred = self.predecessor_codebook(prev_ids.long()) # (B, L, R)
465
+ succ = self.successor_codebook(candidates) # (B, L, k, R)
466
+ scores = unary + torch.einsum("blr,blr,blkr->blk", pred, gate, succ)
467
+ return candidates, scores
468
+
469
+ def select(
470
+ self,
471
+ hidden: torch.Tensor,
472
+ logits: torch.Tensor,
473
+ anchor_ids: torch.Tensor,
474
+ temperature: float,
475
+ ):
476
+ """Inference-time path walk (z-lak reference): greedy at T=0, else sampling
477
+ from softmax over the k candidate scores (also returned for lossless
478
+ rejection sampling). ``anchor_ids`` is the last verified token."""
479
+ unary, candidates = torch.topk(logits, self.top_k, dim=-1, sorted=False)
480
+ hidden = self.hidden_projection(hidden)
481
+ # Accept (B,) or (B, 1) anchor ids (the walk keeps a flat (B,) predecessor).
482
+ predecessor = anchor_ids.reshape(anchor_ids.shape[0], -1).squeeze(-1)
483
+ path, q_rows = [], []
484
+ for position in range(hidden.shape[1]):
485
+ scores = unary[:, position] + torch.einsum(
486
+ "br,bkr->bk",
487
+ self.predecessor_codebook(predecessor) * hidden[:, position],
488
+ self.successor_codebook(candidates[:, position]),
489
+ )
490
+ if temperature > 0:
491
+ q = _sampling_probs(scores[:, None], temperature)[:, 0]
492
+ index = _sample_probs(q)
493
+ q_rows.append(q)
494
+ else:
495
+ index = torch.argmax(scores, dim=-1)
496
+ predecessor = candidates[:, position].gather(-1, index[:, None])[:, 0]
497
+ path.append(predecessor)
498
+ return (
499
+ torch.stack(path, dim=1),
500
+ candidates,
501
+ torch.stack(q_rows, dim=1) if q_rows else None,
502
+ )
503
+
504
+
505
+ # --------------------------------------------------------------------------- #
506
+ # DFlash (1) backbone — copied verbatim from drafttrain/dflash/_vendored/dflash_model.py
507
+ # --------------------------------------------------------------------------- #
508
+
509
+ class DFlashDraftModel(Qwen3PreTrainedModel):
510
+ config_class = Qwen3Config
511
+ _no_split_modules: ClassVar[list[str]] = ["Qwen3DFlashDecoderLayer"]
512
+
513
+ def __init__(self, config) -> None:
514
+ super().__init__(config)
515
+ self.config = config
516
+ self.layers = nn.ModuleList(
517
+ [
518
+ Qwen3DFlashDecoderLayer(config, layer_idx)
519
+ for layer_idx in range(config.num_hidden_layers)
520
+ ]
521
+ )
522
+ dflash_config = getattr(config, "dflash_config", {}) or {}
523
+ self.target_layer_ids = dflash_config.get(
524
+ "target_layer_ids",
525
+ build_target_layer_ids(config.num_target_layers, config.num_hidden_layers),
526
+ )
527
+ self.norm = Qwen3RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
528
+ self.rotary_emb = Qwen3RotaryEmbedding(config)
529
+ self.fc = nn.Linear(
530
+ len(self.target_layer_ids) * config.hidden_size,
531
+ config.hidden_size,
532
+ bias=False,
533
+ )
534
+ self.hidden_norm = Qwen3RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
535
+ self.block_size = config.block_size
536
+ self.mask_token_id = dflash_config.get("mask_token_id", None)
537
+ self.projector_type = dflash_config.get("projector_type", None)
538
+ self.pure_draft_prefix_len = dflash_config.get("pure_draft_prefix_len", 0)
539
+ self.shift_label = dflash_config.get("shift_label", False)
540
+
541
+ if self.projector_type == "domino":
542
+ self.emb_dim = dflash_config["emb_dim"]
543
+ self.gru_hidden_dim = dflash_config["gru_hidden_dim"]
544
+ self.prefix_gru = nn.GRU(
545
+ input_size=config.hidden_size,
546
+ hidden_size=self.gru_hidden_dim,
547
+ num_layers=1,
548
+ batch_first=True,
549
+ bias=False,
550
+ )
551
+ in_dim = config.hidden_size + self.gru_hidden_dim
552
+ self.embed_proj = nn.Sequential(
553
+ nn.Linear(in_dim, self.emb_dim, bias=False),
554
+ nn.SiLU(),
555
+ nn.Linear(self.emb_dim, config.vocab_size, bias=False),
556
+ )
557
+ elif self.projector_type is not None:
558
+ raise ValueError(f"Unknown draft projector_type: {self.projector_type}")
559
+ self.post_init()
560
+
561
+ def forward(
562
+ self,
563
+ position_ids: torch.LongTensor,
564
+ attention_mask: Optional[torch.Tensor] = None,
565
+ noise_embedding: Optional[torch.Tensor] = None,
566
+ target_hidden: Optional[torch.Tensor] = None,
567
+ past_key_values: Optional[Cache] = None,
568
+ use_cache: bool = False,
569
+ **kwargs,
570
+ ) -> CausalLMOutputWithPast:
571
+ hidden_states = noise_embedding
572
+ target_hidden = self.hidden_norm(self.fc(target_hidden))
573
+ position_embeddings = self.rotary_emb(hidden_states, position_ids)
574
+ for layer in self.layers:
575
+ hidden_states = layer(
576
+ hidden_states=hidden_states,
577
+ target_hidden=target_hidden,
578
+ attention_mask=attention_mask,
579
+ position_ids=position_ids,
580
+ past_key_value=past_key_values,
581
+ use_cache=use_cache,
582
+ position_embeddings=position_embeddings,
583
+ **kwargs,
584
+ )
585
+ return self.norm(hidden_states)
586
+
587
+ @torch.inference_mode()
588
+ def spec_generate(
589
+ self,
590
+ target: nn.Module,
591
+ input_ids: torch.LongTensor,
592
+ max_new_tokens: int,
593
+ stop_token_ids: list[int],
594
+ temperature: float,
595
+ ):
596
+ self.eval()
597
+ num_input_tokens = input_ids.shape[1]
598
+ max_length = num_input_tokens + max_new_tokens
599
+
600
+ block_size = self.block_size
601
+ output_ids = torch.full(
602
+ (1, max_length + block_size),
603
+ self.mask_token_id,
604
+ dtype=torch.long,
605
+ device=target.device,
606
+ )
607
+ position_ids = torch.arange(
608
+ output_ids.shape[1], device=target.device
609
+ ).unsqueeze(0)
610
+
611
+ past_key_values_target = DynamicCache()
612
+ past_key_values_draft = DynamicCache()
613
+
614
+ # Prefill stage
615
+ output = target(
616
+ input_ids,
617
+ position_ids=position_ids[:, :num_input_tokens],
618
+ past_key_values=past_key_values_target,
619
+ use_cache=True,
620
+ logits_to_keep=1,
621
+ output_hidden_states=True,
622
+ )
623
+
624
+ output_ids[:, :num_input_tokens] = input_ids
625
+ output_ids[:, num_input_tokens : num_input_tokens + 1] = sample(
626
+ output.logits, temperature
627
+ )
628
+ target_hidden = extract_context_feature(
629
+ output.hidden_states, self.target_layer_ids
630
+ )
631
+
632
+ # Decode stage
633
+ acceptance_lengths = []
634
+ start = input_ids.shape[1]
635
+ while start < max_length:
636
+ block_output_ids = output_ids[:, start : start + block_size].clone()
637
+ block_position_ids = position_ids[:, start : start + block_size]
638
+ noise_embedding = target.model.embed_tokens(block_output_ids)
639
+ draft_logits = target.lm_head(
640
+ self(
641
+ target_hidden=target_hidden,
642
+ noise_embedding=noise_embedding,
643
+ position_ids=position_ids[
644
+ :, past_key_values_draft.get_seq_length() : start + block_size
645
+ ],
646
+ past_key_values=past_key_values_draft,
647
+ use_cache=True,
648
+ is_causal=False,
649
+ )[:, -block_size + 1 :, :]
650
+ )
651
+ past_key_values_draft.crop(start)
652
+ block_output_ids[:, 1:] = sample(draft_logits)
653
+
654
+ output = target(
655
+ block_output_ids,
656
+ position_ids=block_position_ids,
657
+ past_key_values=past_key_values_target,
658
+ use_cache=True,
659
+ output_hidden_states=True,
660
+ )
661
+
662
+ posterior = sample(output.logits, temperature)
663
+ acceptance_length = (
664
+ (block_output_ids[:, 1:] == posterior[:, :-1])
665
+ .cumprod(dim=1)
666
+ .sum(dim=1)[0]
667
+ .item()
668
+ )
669
+ output_ids[:, start : start + acceptance_length + 1] = block_output_ids[
670
+ :, : acceptance_length + 1
671
+ ]
672
+ output_ids[:, start + acceptance_length + 1] = posterior[
673
+ :, acceptance_length
674
+ ]
675
+ start += acceptance_length + 1
676
+ past_key_values_target.crop(start)
677
+ target_hidden = extract_context_feature(
678
+ output.hidden_states, self.target_layer_ids
679
+ )[:, : acceptance_length + 1, :]
680
+ acceptance_lengths.append(acceptance_length + 1)
681
+ if stop_token_ids is not None and any(
682
+ stop_token_id in output_ids[:, num_input_tokens:]
683
+ for stop_token_id in stop_token_ids
684
+ ):
685
+ break
686
+ output_ids = output_ids[:, :max_length]
687
+ output_ids = output_ids[:, output_ids[0] != self.mask_token_id]
688
+ if stop_token_ids is not None:
689
+ stop_token_ids = torch.tensor(stop_token_ids, device=output_ids.device)
690
+ stop_token_indices = torch.isin(
691
+ output_ids[0][num_input_tokens:], stop_token_ids
692
+ ).nonzero(as_tuple=True)[0]
693
+ if stop_token_indices.numel() > 0:
694
+ output_ids = output_ids[
695
+ :, : num_input_tokens + stop_token_indices[0] + 1
696
+ ]
697
+
698
+ return output_ids
699
+
700
+
701
+ # --------------------------------------------------------------------------- #
702
+ # DFlash2 draft model
703
+ # --------------------------------------------------------------------------- #
704
+
705
+ class DFlash2DraftModel(DFlashDraftModel):
706
+ """DFlash backbone with the two DFlash2 additions.
707
+
708
+ - Every decoder layer gets ``attention_conv`` + ``mlp_conv``
709
+ (GroupedDynamicCausalConv, kernel_size=2 / group_size=16 by default).
710
+ - A ``candidate_selector`` (CandidateSelector, rank=256 / top_k=16 by default).
711
+
712
+ Config keys (in ``dflash_config``): ``conv_kernel_size``, ``conv_group_size``,
713
+ ``conv_identity_init``, ``selector_rank``, ``selector_top_k`` — mirroring the
714
+ released z-lab/Qwen3.8-27B-DFlash2 config.json (which carries the first two and
715
+ the last two; ``conv_identity_init`` is a training-only knob).
716
+
717
+ Weight layout is z-lab-compatible, so exported checkpoints load under SGLang's
718
+ DFlash2 support and under z-lab/dflash's reference implementation.
719
+ """
720
+
721
+ @classmethod
722
+ def from_pretrained(cls, *args, **kwargs):
723
+ # The SERVED checkpoint stores the selector codebooks under bare keys
724
+ # (no ".weight" — z-lab/SGLang format; see export_draft.py). Map them onto
725
+ # this model's nn.Embedding parameters when loading via HF transformers.
726
+ kwargs.setdefault(
727
+ "key_mapping",
728
+ {
729
+ f"candidate_selector.{name}": f"candidate_selector.{name}.weight"
730
+ for name in ("predecessor_codebook", "successor_codebook")
731
+ },
732
+ )
733
+ return super().from_pretrained(*args, **kwargs)
734
+
735
+ def __init__(self, config) -> None:
736
+ super().__init__(config)
737
+ dflash_config = _dflash_config(config)
738
+ kernel_size = int(dflash_config.get("conv_kernel_size", 2))
739
+ group_size = int(dflash_config.get("conv_group_size", 16))
740
+ self.conv_identity_init = bool(dflash_config.get("conv_identity_init", True))
741
+ for layer in self.layers:
742
+ layer.attention_conv = GroupedDynamicCausalConv(
743
+ config.hidden_size, kernel_size, group_size, self.block_size
744
+ )
745
+ layer.mlp_conv = GroupedDynamicCausalConv(
746
+ config.hidden_size, kernel_size, group_size, self.block_size
747
+ )
748
+ self.candidate_selector = CandidateSelector(config)
749
+ # post_init() initializes the newly added Linear/Embedding modules (already
750
+ # initialized parent modules are skipped); the raw base_kernel Parameters and
751
+ # the identity pattern are then set explicitly so initialization does not
752
+ # depend on transformers' double-post_init semantics.
753
+ self.post_init()
754
+ self._init_dflash2_weights()
755
+
756
+ def _init_dflash2_weights(self) -> None:
757
+ std = float(getattr(self.config, "initializer_range", 0.02))
758
+ with torch.no_grad():
759
+ for layer in self.layers:
760
+ for conv in (layer.attention_conv, layer.mlp_conv):
761
+ if conv is None: # pragma: no cover (always set for DFlash2)
762
+ continue
763
+ # Static taps start as identity: tap-0 (self) = 1, tap-1
764
+ # (predecessor) = 0. Anything else destroys the residual stream at
765
+ # init (a zero tap-0 zeroes the sublayer input).
766
+ conv.base_kernel.zero_()
767
+ conv.base_kernel[:, 0, :].fill_(1.0)
768
+ if self.conv_identity_init:
769
+ conv.kernel_projection.weight.zero_()
770
+ else:
771
+ conv.kernel_projection.weight.normal_(mean=0.0, std=std)
772
+ sel = self.candidate_selector
773
+ sel.predecessor_codebook.weight.normal_(mean=0.0, std=std)
774
+ sel.successor_codebook.weight.normal_(mean=0.0, std=std)
775
+ sel.hidden_projection.weight.normal_(mean=0.0, std=std)
776
+
777
+ def propose(
778
+ self,
779
+ hidden: torch.Tensor,
780
+ anchor_ids: torch.Tensor,
781
+ output_head: nn.Module,
782
+ temperature: float,
783
+ ):
784
+ """Select a draft path through the top-k candidates (SGLang/spec entry point)."""
785
+ logits = output_head(hidden)
786
+ return self.candidate_selector.select(hidden, logits, anchor_ids, temperature)
787
+
788
+ @torch.inference_mode()
789
+ def spec_generate(
790
+ self,
791
+ target: nn.Module,
792
+ input_ids: torch.LongTensor,
793
+ max_new_tokens: int,
794
+ stop_token_ids: list[int],
795
+ temperature: float,
796
+ ):
797
+ """DFlash1 spec_generate with the candidate-selector walk replacing the
798
+ independent per-position argmax/sample (the only behavioral change)."""
799
+ self.eval()
800
+ num_input_tokens = input_ids.shape[1]
801
+ max_length = num_input_tokens + max_new_tokens
802
+
803
+ block_size = self.block_size
804
+ output_ids = torch.full(
805
+ (1, max_length + block_size),
806
+ self.mask_token_id,
807
+ dtype=torch.long,
808
+ device=target.device,
809
+ )
810
+ position_ids = torch.arange(
811
+ output_ids.shape[1], device=target.device
812
+ ).unsqueeze(0)
813
+
814
+ past_key_values_target = DynamicCache()
815
+ past_key_values_draft = DynamicCache()
816
+
817
+ # Prefill stage
818
+ output = target(
819
+ input_ids,
820
+ position_ids=position_ids[:, :num_input_tokens],
821
+ past_key_values=past_key_values_target,
822
+ use_cache=True,
823
+ logits_to_keep=1,
824
+ output_hidden_states=True,
825
+ )
826
+
827
+ output_ids[:, :num_input_tokens] = input_ids
828
+ output_ids[:, num_input_tokens : num_input_tokens + 1] = sample(
829
+ output.logits, temperature
830
+ )
831
+ target_hidden = extract_context_feature(
832
+ output.hidden_states, self.target_layer_ids
833
+ )
834
+
835
+ # Decode stage
836
+ acceptance_lengths = []
837
+ start = input_ids.shape[1]
838
+ while start < max_length:
839
+ block_output_ids = output_ids[:, start : start + block_size].clone()
840
+ block_position_ids = position_ids[:, start : start + block_size]
841
+ noise_embedding = target.model.embed_tokens(block_output_ids)
842
+ draft_hidden = self(
843
+ target_hidden=target_hidden,
844
+ noise_embedding=noise_embedding,
845
+ position_ids=position_ids[
846
+ :, past_key_values_draft.get_seq_length() : start + block_size
847
+ ],
848
+ past_key_values=past_key_values_draft,
849
+ use_cache=True,
850
+ is_causal=False,
851
+ )[:, -block_size + 1 :, :]
852
+ past_key_values_draft.crop(start)
853
+ draft_logits = target.lm_head(draft_hidden)
854
+ # DFlash2: walk one coherent path through the top-k candidates instead of
855
+ # taking each position's top pick independently.
856
+ draft_tokens, _, _ = self.candidate_selector.select(
857
+ draft_hidden,
858
+ draft_logits,
859
+ block_output_ids[:, 0],
860
+ temperature,
861
+ )
862
+ block_output_ids[:, 1:] = draft_tokens
863
+
864
+ output = target(
865
+ block_output_ids,
866
+ position_ids=block_position_ids,
867
+ past_key_values=past_key_values_target,
868
+ use_cache=True,
869
+ output_hidden_states=True,
870
+ )
871
+
872
+ posterior = sample(output.logits, temperature)
873
+ acceptance_length = (
874
+ (block_output_ids[:, 1:] == posterior[:, :-1])
875
+ .cumprod(dim=1)
876
+ .sum(dim=1)[0]
877
+ .item()
878
+ )
879
+ output_ids[:, start : start + acceptance_length + 1] = block_output_ids[
880
+ :, : acceptance_length + 1
881
+ ]
882
+ output_ids[:, start + acceptance_length + 1] = posterior[
883
+ :, acceptance_length
884
+ ]
885
+ start += acceptance_length + 1
886
+ past_key_values_target.crop(start)
887
+ target_hidden = extract_context_feature(
888
+ output.hidden_states, self.target_layer_ids
889
+ )[:, : acceptance_length + 1, :]
890
+ acceptance_lengths.append(acceptance_length + 1)
891
+ if stop_token_ids is not None and any(
892
+ stop_token_id in output_ids[:, num_input_tokens:]
893
+ for stop_token_id in stop_token_ids
894
+ ):
895
+ break
896
+ output_ids = output_ids[:, :max_length]
897
+ output_ids = output_ids[:, output_ids[0] != self.mask_token_id]
898
+ if stop_token_ids is not None:
899
+ stop_token_ids = torch.tensor(stop_token_ids, device=output_ids.device)
900
+ stop_token_indices = torch.isin(
901
+ output_ids[0][num_input_tokens:], stop_token_ids
902
+ ).nonzero(as_tuple=True)[0]
903
+ if stop_token_indices.numel() > 0:
904
+ output_ids = output_ids[
905
+ :, : num_input_tokens + stop_token_indices[0] + 1
906
+ ]
907
+
908
+ return output_ids
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8114ddb576d991afae44de2f9aebc56921ddada5d698528749bcaf1da81b48ea
3
+ size 1010529880
preprocessor_config.json ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "size": {
3
+ "longest_edge": 16777216,
4
+ "shortest_edge": 65536
5
+ },
6
+ "patch_size": 16,
7
+ "temporal_patch_size": 2,
8
+ "merge_size": 2,
9
+ "image_mean": [
10
+ 0.5,
11
+ 0.5,
12
+ 0.5
13
+ ],
14
+ "image_std": [
15
+ 0.5,
16
+ 0.5,
17
+ 0.5
18
+ ],
19
+ "processor_class": "Qwen3VLProcessor",
20
+ "image_processor_type": "Qwen2VLImageProcessorFast"
21
+ }
processor_config.json ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "image_processor": {
3
+ "do_convert_rgb": true,
4
+ "do_normalize": true,
5
+ "do_rescale": true,
6
+ "do_resize": true,
7
+ "image_mean": [
8
+ 0.5,
9
+ 0.5,
10
+ 0.5
11
+ ],
12
+ "image_processor_type": "Qwen2VLImageProcessor",
13
+ "image_std": [
14
+ 0.5,
15
+ 0.5,
16
+ 0.5
17
+ ],
18
+ "merge_size": 2,
19
+ "patch_size": 16,
20
+ "resample": 3,
21
+ "rescale_factor": 0.00392156862745098,
22
+ "size": {
23
+ "longest_edge": 16777216,
24
+ "shortest_edge": 65536
25
+ },
26
+ "temporal_patch_size": 2
27
+ },
28
+ "processor_class": "Qwen3VLProcessor",
29
+ "video_processor": {
30
+ "do_convert_rgb": true,
31
+ "do_normalize": true,
32
+ "do_rescale": true,
33
+ "do_resize": true,
34
+ "do_sample_frames": true,
35
+ "fps": 2,
36
+ "image_mean": [
37
+ 0.5,
38
+ 0.5,
39
+ 0.5
40
+ ],
41
+ "image_std": [
42
+ 0.5,
43
+ 0.5,
44
+ 0.5
45
+ ],
46
+ "max_frames": 768,
47
+ "merge_size": 2,
48
+ "min_frames": 4,
49
+ "patch_size": 16,
50
+ "resample": 3,
51
+ "rescale_factor": 0.00392156862745098,
52
+ "return_metadata": false,
53
+ "size": {
54
+ "longest_edge": 25165824,
55
+ "shortest_edge": 4096
56
+ },
57
+ "temporal_patch_size": 2,
58
+ "video_processor_type": "Qwen3VLVideoProcessor"
59
+ }
60
+ }
tokenizer.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:06b9509352d2af50381ab2247e083b80d32d5c0aba91c272ca9ff729b6a0e523
3
+ size 19989325
tokenizer_config.json ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "add_prefix_space": false,
3
+ "audio_bos_token": "<|audio_start|>",
4
+ "audio_eos_token": "<|audio_end|>",
5
+ "audio_token": "<|audio_pad|>",
6
+ "backend": "tokenizers",
7
+ "bos_token": null,
8
+ "clean_up_tokenization_spaces": false,
9
+ "eos_token": "<|im_end|>",
10
+ "errors": "replace",
11
+ "image_token": "<|image_pad|>",
12
+ "is_local": true,
13
+ "local_files_only": false,
14
+ "model_max_length": 262144,
15
+ "model_specific_special_tokens": {
16
+ "audio_bos_token": "<|audio_start|>",
17
+ "audio_eos_token": "<|audio_end|>",
18
+ "audio_token": "<|audio_pad|>",
19
+ "image_token": "<|image_pad|>",
20
+ "video_token": "<|video_pad|>",
21
+ "vision_bos_token": "<|vision_start|>",
22
+ "vision_eos_token": "<|vision_end|>"
23
+ },
24
+ "pad_token": "<|endoftext|>",
25
+ "pretokenize_regex": "(?i:'s|'t|'re|'ve|'m|'ll|'d)|[^\\r\\n\\p{L}\\p{N}]?[\\p{L}\\p{M}]+|\\p{N}| ?[^\\s\\p{L}\\p{M}\\p{N}]+[\\r\\n]*|\\s*[\\r\\n]+|\\s+(?!\\S)|\\s+",
26
+ "processor_class": "Qwen3VLProcessor",
27
+ "split_special_tokens": false,
28
+ "tokenizer_class": "Qwen2Tokenizer",
29
+ "unk_token": null,
30
+ "video_token": "<|video_pad|>",
31
+ "vision_bos_token": "<|vision_start|>",
32
+ "vision_eos_token": "<|vision_end|>"
33
+ }