Skip to content

Commit 429d812

Browse files
authored
Merge pull request sgl-project#592 from catnanami/feature/vp-drafter-training
[Feature] Add VP drafter training mode for DFlash
2 parents 5c20b03 + 5e37f68 commit 429d812

5 files changed

Lines changed: 294 additions & 8 deletions

File tree

configs/qwen3-8b-dta.json

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
{
2+
"architectures": [
3+
"DFlashDraftModel"
4+
],
5+
"attention_bias": false,
6+
"attention_dropout": 0.0,
7+
"auto_map": {
8+
"AutoModel": "dflash.DFlashDraftModel"
9+
},
10+
"block_size": 16,
11+
"bos_token_id": 151643,
12+
"dflash_config": {
13+
"mask_token_id": 151669,
14+
"target_layer_ids": [1, 9, 17, 25, 33],
15+
"training_mode": "vp_drafter",
16+
"prefix_weight_base": 0.9
17+
},
18+
"dtype": "bfloat16",
19+
"eos_token_id": 151645,
20+
"head_dim": 128,
21+
"hidden_act": "silu",
22+
"hidden_size": 4096,
23+
"initializer_range": 0.02,
24+
"intermediate_size": 12288,
25+
"layer_types": [
26+
"full_attention",
27+
"full_attention",
28+
"full_attention",
29+
"full_attention",
30+
"full_attention"
31+
],
32+
"max_position_embeddings": 40960,
33+
"max_window_layers": 5,
34+
"model_type": "qwen3",
35+
"num_attention_heads": 32,
36+
"num_hidden_layers": 5,
37+
"num_key_value_heads": 8,
38+
"num_target_layers": 36,
39+
"rms_norm_eps": 1e-06,
40+
"rope_scaling": null,
41+
"rope_theta": 1000000,
42+
"sliding_window": null,
43+
"tie_word_embeddings": false,
44+
"use_cache": true,
45+
"use_sliding_window": false,
46+
"vocab_size": 151936
47+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#!/bin/bash
2+
3+
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
4+
ROOT_DIR=$(dirname $SCRIPT_DIR)
5+
export TORCHINDUCTOR_CACHE_DIR=$ROOT_DIR/cache/compiled_kernels
6+
export SPECFORGE_DATA_NUM_PROC=32
7+
NUM_GPUS=${1:-8}
8+
9+
ATTENTION_BACKEND=${2:-flex_attention}
10+
11+
torchrun \
12+
--standalone \
13+
--nproc_per_node $NUM_GPUS \
14+
$ROOT_DIR/scripts/train_dflash.py \
15+
--target-model-path Qwen/Qwen3-8B \
16+
--target-model-backend sglang \
17+
--draft-config-path $ROOT_DIR/configs/qwen3-8b-dta.json \
18+
--train-data-path $ROOT_DIR/cache/dataset/perfectblend_qwen3-8b_regen.jsonl \
19+
--output-dir $ROOT_DIR/outputs/qwen3-8b-dta-perfectblend \
20+
--num-epochs 6 \
21+
--batch-size 4 \
22+
--learning-rate 6e-4 \
23+
--warmup-ratio 0.04 \
24+
--max-grad-norm 1.0 \
25+
--max-length 3072 \
26+
--chat-template qwen \
27+
--attention-backend $ATTENTION_BACKEND \
28+
--loss-decay-gamma 7.0 \
29+
--log-interval 50 \
30+
--save-interval 1000 \
31+
--report-to wandb \
32+
--wandb-project specforge-qwen3-8b-dta \
33+
--block-size 16 \
34+
--num-anchors 512 \
35+
--wandb-name qwen3-8b-dta-perfectblend

scripts/train_dflash.py

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,21 +93,34 @@ def parse_args():
9393
model_group.add_argument(
9494
"--loss-type",
9595
type=str,
96-
default="dflash",
96+
default=None,
9797
choices=[
9898
"dflash",
99+
"vp_drafter",
99100
"dpace",
100101
"dpace-cumulative-confidence-only",
101102
"dpace-continuation-value-only",
102103
],
103-
help=("Loss variant. Use dpace for Dynamic Position-Aware Cross-Entropy."),
104+
help=(
105+
"Training objective. If omitted, reads dflash_config.training_mode or "
106+
"dflash_config.loss_type from the draft config, defaulting to dflash."
107+
),
104108
)
105109
model_group.add_argument(
106110
"--dpace-alpha",
107111
type=float,
108112
default=0.5,
109113
help="Smoothing alpha for D-PACE position weights.",
110114
)
115+
model_group.add_argument(
116+
"--prefix-weight-base",
117+
type=float,
118+
default=None,
119+
help=(
120+
"VP-Drafter prefix length sampling base. Values below 1 prefer shorter "
121+
"visible prefixes; defaults to dflash_config.prefix_weight_base or 0.9."
122+
),
123+
)
111124
model_group.add_argument(
112125
"--embedding-key",
113126
type=str,
@@ -219,8 +232,20 @@ def build_models(args) -> Tuple[DFlashTargetModel, DFlashDraftModel]:
219232
if not hasattr(draft_config, "dflash_config") or draft_config.dflash_config is None:
220233
draft_config.dflash_config = {}
221234

235+
args.loss_type = (
236+
args.loss_type
237+
or draft_config.dflash_config.get("training_mode")
238+
or draft_config.dflash_config.get("loss_type")
239+
or "dflash"
240+
)
241+
if args.prefix_weight_base is None:
242+
args.prefix_weight_base = draft_config.dflash_config.get(
243+
"prefix_weight_base", 0.9
244+
)
245+
222246
draft_config._attn_implementation = args.attention_backend
223247
print_on_rank0(f"Using attention backend: {args.attention_backend}")
248+
print_on_rank0(f"Using DFlash training loss_type: {args.loss_type}")
224249

225250
draft_model = DFlashDraftModel(draft_config).to(device=device, dtype=torch.bfloat16)
226251

@@ -454,6 +479,8 @@ def main():
454479
print_on_rank0(f"Total training steps: {total_steps}")
455480

456481
print_on_rank0("Loading target embeddings and head...")
482+
device = get_local_device()
483+
device_type = device.type
457484
target_components = TargetEmbeddingsAndHead.from_pretrained(
458485
args.target_model_path,
459486
embed_key=args.embedding_key,
@@ -473,6 +500,7 @@ def main():
473500
loss_decay_gamma=args.loss_decay_gamma,
474501
loss_type=args.loss_type,
475502
dpace_alpha=args.dpace_alpha,
503+
prefix_weight_base=args.prefix_weight_base,
476504
)
477505

478506
# Wrap each transformer block as its own FSDP unit so that all-gather /

specforge/core/dflash.py

Lines changed: 98 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,12 @@
2525

2626
_VALID_LOSS_TYPES = {
2727
"dflash",
28+
"vp_drafter",
2829
"dpace",
2930
"dpace-cumulative-confidence-only",
3031
"dpace-continuation-value-only",
3132
}
32-
_DPACE_LOSS_TYPES = _VALID_LOSS_TYPES - {"dflash"}
33+
_DPACE_LOSS_TYPES = _VALID_LOSS_TYPES - {"dflash", "vp_drafter"}
3334

3435

3536
def create_dflash_sdpa_mask(anchor_positions, block_keep_mask, S, block_size, device):
@@ -121,6 +122,7 @@ def __init__(
121122
loss_decay_gamma: Optional[float] = None,
122123
loss_type: str = "dflash",
123124
dpace_alpha: float = 0.5,
125+
prefix_weight_base: float = 0.9,
124126
):
125127
super().__init__()
126128
if loss_type not in _VALID_LOSS_TYPES:
@@ -129,6 +131,12 @@ def __init__(
129131
)
130132
if not 0.0 <= dpace_alpha <= 1.0:
131133
raise ValueError(f"dpace_alpha must be in [0, 1], got {dpace_alpha}")
134+
if prefix_weight_base is None:
135+
prefix_weight_base = 0.9
136+
if prefix_weight_base <= 0.0:
137+
raise ValueError(
138+
f"prefix_weight_base must be positive, got {prefix_weight_base}"
139+
)
132140

133141
self.draft_model = draft_model
134142
self.lm_head = target_lm_head
@@ -140,6 +148,7 @@ def __init__(
140148
self.loss_decay_gamma = loss_decay_gamma
141149
self.loss_type = loss_type
142150
self.dpace_alpha = dpace_alpha
151+
self.prefix_weight_base = prefix_weight_base
143152

144153
self._cached_block_mask: Optional[BlockMask] = None
145154
self._cached_seq_len: Optional[int] = None
@@ -183,6 +192,33 @@ def _sample_anchor_positions(
183192

184193
return anchors, keep_mask
185194

195+
def _sample_prefix_lengths(
196+
self, bsz: int, n_blocks: int, device: torch.device
197+
) -> torch.Tensor:
198+
"""Sample visible prefix lengths for VP-Drafter training.
199+
200+
A prefix length i means block positions [0, i) are visible real tokens and
201+
positions [i, block_size) are masked prediction targets. The sampled
202+
range follows D2SD's variable-prefix recipe while avoiding the degenerate
203+
fixed-anchor DFlash case.
204+
"""
205+
min_prefix = min(2, self.block_size - 1)
206+
max_prefix = self.block_size - 1
207+
if max_prefix <= min_prefix:
208+
return torch.full(
209+
(bsz, n_blocks), min_prefix, dtype=torch.long, device=device
210+
)
211+
212+
prefix_ids = torch.arange(min_prefix, max_prefix + 1, device=device)
213+
weights = torch.pow(
214+
torch.full_like(prefix_ids, self.prefix_weight_base, dtype=torch.float32),
215+
prefix_ids.float(),
216+
)
217+
samples = torch.multinomial(
218+
weights, num_samples=bsz * n_blocks, replacement=True
219+
).reshape(bsz, n_blocks)
220+
return samples + min_prefix
221+
186222
def prepare_noise_input(
187223
self, input_ids: torch.Tensor, block_ids: Optional[torch.Tensor] = None
188224
) -> torch.Tensor:
@@ -235,6 +271,36 @@ def _create_noise_embed(self, input_ids, anchor_positions, block_keep_mask):
235271

236272
return self.embed_tokens(noise_ids)
237273

274+
def _create_vp_noise_embed(
275+
self,
276+
input_ids: torch.Tensor,
277+
anchor_positions: torch.Tensor,
278+
block_keep_mask: torch.Tensor,
279+
prefix_lengths: torch.Tensor,
280+
) -> torch.Tensor:
281+
"""Prepare VP-Drafter inputs with variable visible prefixes."""
282+
bsz, seq_len = input_ids.shape
283+
n = anchor_positions.shape[1]
284+
bs = self.block_size
285+
device = input_ids.device
286+
287+
offsets = torch.arange(bs, device=device).view(1, 1, -1)
288+
token_positions = anchor_positions.unsqueeze(-1) + offsets
289+
safe_positions = token_positions.clamp(0, seq_len - 1)
290+
291+
real_tokens = torch.gather(
292+
input_ids.unsqueeze(1).expand(-1, n, -1),
293+
2,
294+
safe_positions,
295+
)
296+
visible_prefix = offsets < prefix_lengths.unsqueeze(-1)
297+
valid_positions = token_positions < seq_len
298+
fill_mask = visible_prefix & block_keep_mask.unsqueeze(-1) & valid_positions
299+
300+
mask_tokens = torch.full_like(real_tokens, self.mask_token_id)
301+
noise_ids = torch.where(fill_mask, real_tokens, mask_tokens)
302+
return self.embed_tokens(noise_ids.reshape(bsz, n * bs))
303+
238304
def _dpace_weight(
239305
self,
240306
prob: torch.Tensor,
@@ -285,9 +351,18 @@ def forward(
285351
seq_len, loss_mask, device
286352
)
287353

288-
noise_embedding = self._create_noise_embed(
289-
input_ids, anchor_positions, block_keep_mask
290-
)
354+
prefix_lengths = None
355+
if self.loss_type == "vp_drafter":
356+
prefix_lengths = self._sample_prefix_lengths(
357+
bsz, anchor_positions.shape[1], device
358+
)
359+
noise_embedding = self._create_vp_noise_embed(
360+
input_ids, anchor_positions, block_keep_mask, prefix_lengths
361+
)
362+
else:
363+
noise_embedding = self._create_noise_embed(
364+
input_ids, anchor_positions, block_keep_mask
365+
)
291366

292367
context_position_ids = (
293368
torch.arange(seq_len, device=device).unsqueeze(0).expand(bsz, -1)
@@ -340,7 +415,12 @@ def forward(
340415
weight_mask = weight_mask * valid_label_mask.float()
341416

342417
pos_in_block = torch.arange(self.block_size, device=device).view(1, 1, -1)
343-
weight_mask = weight_mask * (pos_in_block > 0).float()
418+
if self.loss_type == "vp_drafter":
419+
weight_mask = (
420+
weight_mask * (pos_in_block >= prefix_lengths.unsqueeze(-1)).float()
421+
)
422+
else:
423+
weight_mask = weight_mask * (pos_in_block > 0).float()
344424

345425
original_loss_mask_gathered = torch.gather(
346426
loss_mask.unsqueeze(1).expand(-1, anchor_positions.size(1), -1),
@@ -367,6 +447,19 @@ def forward(
367447
)
368448
loss_weights = loss_weights * decay_weights
369449

450+
flat_weights = loss_weights.view(-1)
451+
valid_token_count = flat_weights.sum() + 1e-6
452+
loss = (loss_per_token * flat_weights).sum() / valid_token_count
453+
elif self.loss_type == "vp_drafter":
454+
loss_weights = weight_mask
455+
if self.loss_decay_gamma is not None and self.loss_decay_gamma > 0:
456+
k = torch.arange(self.block_size, device=device).view(1, 1, -1)
457+
effective_pos = (
458+
k.float() - prefix_lengths.unsqueeze(-1).float()
459+
).clamp(min=0)
460+
decay_weights = torch.exp(-effective_pos / self.loss_decay_gamma)
461+
loss_weights = loss_weights * decay_weights
462+
370463
flat_weights = loss_weights.view(-1)
371464
valid_token_count = flat_weights.sum() + 1e-6
372465
loss = (loss_per_token * flat_weights).sum() / valid_token_count

0 commit comments

Comments
 (0)