Skip to content

Commit 21033a7

Browse files
weifanjiangWeifan JiangWeifan Jiang
authored
Add D-PACE loss implementation for D-Flash training (#736)
<!-- markdownlint-disable --> <!-- PLEASE FILL IN THE PR DESCRIPTION HERE ENSURING ALL CHECKLIST ITEMS (AT THE BOTTOM) HAVE BEEN CONSIDERED. --> ## Purpose This PR implements [D-PACE: Dynamic Position-Aware Cross-Entropy for Parallel Speculative Drafting](https://arxiv.org/abs/2605.18810). D-PACE proposes an alternative per-position loss weighting for training block-parallel draft models. Instead of applying a fixed exponential decay over block positions, D-PACE dynamically adjusts the weights as training progresses. This is based on the intuition that although earlier positions are naturally more important (i.e., an early rejection forces all subsequent draft positions to be discarded), when they are sufficiently stabilized, the training priority should shift to later positions to improve overall acceptance rate. Two arguments are added to `train.py` to support D-PACE loss: - `--per-position-loss-weight ["fixed-exp-decay", "dpace"]` (default `fixed-exp-decay`). This flag enables D-PACE loss; while missing, it defaults to the original fixed exponential decay. - `--dpace-alpha` (default `0.5`). This flag is only effective when D-PACE is enabled. It smoothes per-position confidence according to equation 7 of the paper. The paper uses 0.5 as the default value for their evaluation and justifies this choice in sensitivity analysis. Note that D-PACE requires cross-entropy as the training loss (`--loss-fn ce`), allowing per-position confidence score computation to be piggybacked on the loss computation. Applying D-PACE to other losses are not supported and will result in `ValueError`. D-PACE is currently enabled for D-Flash and D-Spark draft models. Theoretically, it is compatible with all block-parallel drafters. ## Tests **D-Flash + D-PACE evaluation.** Multiple D-Flash checkpoints are trained with different loss function configurations: - `--loss-fn kld`: KL Divergence (current default) - `--loss-fn ce`: Cross Entropy with fixed exponential decay - `--per-position-loss-weight dpace --dpace-alpha ALPHA`: D-PACE with `ALPHA` = 0.3, 0.5, 0.7. The training set is 100K random samples from Magpie+Ultrachat. The verifier model is Qwen3-8B. Each checkpoint uses 1 A100 for hidden state generation and 7 A100s for training. Training configurations are identical for all checkpoints: ``` --target-layer-ids 1 9 17 25 34 --num-layers 5 --draft-vocab-size 32000 --max-anchors 3072 --block-size 8 --sliding-window 2048 --sliding-window-indices 0 1 2 3 4 --scheduler-type cosine --lr 0.0006 --epochs 1 --noise-std 0.0 ``` Metric: since D-PACE is a pure training-time optimization and does not change draft model's forward pass complexity, its gains are captured entirely by acceptance rates. Evaluation results: Mean-Accepted-Length (MAL) on [Speculator Benchmarks](https://huggingface.co/datasets/RedHatAI/speculator_benchmarks): | Dataset | KL Divergence | CE | D-PACE $\alpha=0.3$ | D-PACE $\alpha=0.5$| D-PACE $\alpha=0.7$ | |---|---:|---:|---:|---:|---:| | HumanEval | 2.977 | 2.956 | 3.047 | **3.169** | 3.139 | | math_reasoning | 3.275 | 3.322 | **3.541** | 3.532 | 3.531 | | qa | 2.279 | 2.343 | 2.455 | **2.495** | 2.431 | | question | 2.585 | 2.567 | 2.685 | 2.680 | **2.708** | | rag | 2.302 | 2.269 | **2.393** | 2.372 | 2.336 | | summarization | 1.906 | 1.918 | 1.981 | 1.980 | **1.991** | | tool_call | 2.372 | 2.327 | **2.480** | 2.454 | 2.467 | | translation | 2.096 | 2.072 | 2.176 | 2.183 | **2.191** | | writing | 2.565 | 2.586 | 2.683 | 2.684 | **2.718** | | Mean | 2.484 | 2.485 | 2.605 | **2.617** | 2.612 | **D-Spark + D-PACE evaluation.** D-Spark checkpoints use Markov rank 256 and vanilla Markov head type. The confidence head is disabled. Remaining training configurations match above D-Flash experiments. - `--loss-fn ce`: Vanilla fixed-decay cross-entropy - `--position-loss-weight dpace --dpace-alpha 0.5`: D-PACE with $\alpha=0.5$ MALs: | Dataset | CE | D-PACE $\alpha=0.5$ | |---|---:|---:| | HumanEval | 3.145 | **3.302** | | math_reasoning | 3.457 | **3.615** | | qa | 2.352 | **2.492** | | question | 2.671 | **2.770** | | rag | 2.276 | **2.384** | | summarization | 1.916 | **1.996** | | tool_call | 2.451 | **2.538** | | translation | 2.082 | **2.202** | | writing | 2.686 | **2.776** | | Mean | 2.560 | **2.675** | D-PACE improves upon fixed-decay loss for both D-Flash and D-Spark drafters. ## Checklist I have filled in: - [x] The purpose of the PR, such as "Fix some issue (link existing issues this PR will resolve)". - [x] The test plan/results, such as providing test command and pasting the results. - [ ] (Optional) The necessary documentation update. - [x] I (a human) have written or reviewed the code in this pr to the best of my ability. --------- Signed-off-by: Weifan Jiang <weijiang@redhat.com> Co-authored-by: Weifan Jiang <weifanjiang@a100-08.nemg-001.lab.rdu2.dc.redhat.com> Co-authored-by: Weifan Jiang <weijiang@redhat.com>
1 parent 49b38a4 commit 21033a7

6 files changed

Lines changed: 137 additions & 10 deletions

File tree

scripts/train.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -961,6 +961,20 @@ def parse_args():
961961
default=4.0,
962962
help="Decay gamma for DFlash/DSpark loss weighting (default: 4.0)",
963963
)
964+
# D-Pace specific arguments (loss weight option + smoothing)
965+
parser.add_argument(
966+
"--per-position-loss-weight",
967+
choices=["fixed-exp-decay", "dpace"],
968+
default="fixed-exp-decay",
969+
help="Per-position loss weight option for D-PACE support"
970+
"default: fixed-exp-decay",
971+
)
972+
parser.add_argument(
973+
"--dpace-alpha",
974+
type=float,
975+
default=0.5,
976+
help="Smoothing constant for D-PACE loss (default: 0.5)",
977+
)
964978
# DSpark-specific arguments (sequential Markov head + confidence head).
965979
parser.add_argument(
966980
"--markov-rank",
@@ -1132,6 +1146,13 @@ def parse_args():
11321146
provided = explicitly_provided_dests(parser, DECODER_SHAPING_FLAGS)
11331147
validate_draft_init_args(parser, args, provided)
11341148
resolve_loss_config(args.loss_fn)
1149+
1150+
if args.per_position_loss_weight == "dpace":
1151+
if args.loss_fn != "ce":
1152+
parser.error("--per-position-loss-weight=dpace requires --loss-fn=ce")
1153+
if not 0.0 < args.dpace_alpha <= 1.0:
1154+
raise ValueError(f"alpha must be in (0, 1], got {args.dpace_alpha}")
1155+
11351156
return args
11361157

11371158

src/speculators/models/dflash/core.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,10 +207,16 @@ def get_trainer_kwargs(**kwargs) -> tuple[dict, dict]:
207207
loss_config = resolve_loss_config(kwargs["loss_fn"])
208208
gamma = kwargs.get("dflash_decay_gamma", 4.0)
209209
max_anchors = kwargs.get("max_anchors", 3072)
210+
per_position_loss_weight = kwargs.get(
211+
"per_position_loss_weight", "fixed-exp-decay"
212+
)
213+
dpace_alpha = kwargs.get("dpace_alpha", 0.5)
210214
shared = {
211215
"loss_config": loss_config,
212216
"gamma": gamma,
213217
"max_anchors": max_anchors,
218+
"per_position_loss_weight": per_position_loss_weight,
219+
"dpace_alpha": dpace_alpha,
214220
}
215221
return dict(shared), dict(shared)
216222

@@ -393,6 +399,8 @@ def forward(
393399
loss_config: LossConfig | None = None,
394400
gamma: float = 4.0,
395401
max_anchors: int = 3072,
402+
per_position_loss_weight: str = "fixed-exp-decay",
403+
dpace_alpha: float = 0.5,
396404
**kwargs,
397405
):
398406
_, logits, targets, aligned_loss_mask, _ = self._backbone_forward(
@@ -412,6 +420,8 @@ def forward(
412420
self.block_size,
413421
gamma=gamma,
414422
loss_config=loss_config,
423+
per_position_loss_weight=per_position_loss_weight,
424+
dpace_alpha=dpace_alpha,
415425
)
416426
draft_tokens = torch.argmax(logits, dim=-1)
417427

src/speculators/models/dflash/metrics.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
compound_loss,
1111
compute_accuracy_multi_step,
1212
dflash_loss_decay,
13+
dpace_loss_decay,
1314
kl_div_loss,
1415
)
1516

@@ -23,6 +24,8 @@ def compute_metrics(
2324
block_size: int = 1,
2425
gamma: float = 4.0,
2526
loss_config: LossConfig | None = None,
27+
per_position_loss_weight: str = "fixed-exp-decay",
28+
dpace_alpha: float = 0.5,
2629
) -> tuple[torch.Tensor, dict]:
2730
"""Compute loss and accuracy metrics for draft model predictions.
2831
@@ -33,6 +36,8 @@ def compute_metrics(
3336
block_size: Block size for per-position metrics
3437
gamma: Temperature for exponential decay in loss weighting
3538
loss_config: Mapping of ``{name: (loss_fn, weight)}``
39+
per_position_loss_weight: Weighting option for per-position block-drafting loss
40+
dpace_alpha: Smoothing constant for D-Pace loss weighting
3641
3742
Returns:
3843
Tuple of (loss, metrics_dict) where metrics_dict contains:
@@ -47,13 +52,23 @@ def compute_metrics(
4752
pos_idx = torch.arange(seq_len, device=logits.device) % block_size
4853
pos_idx = pos_idx.unsqueeze(0) # shape: [1, T]
4954

55+
if per_position_loss_weight == "dpace":
56+
decay_fn = partial(
57+
dpace_loss_decay,
58+
loss_mask=loss_mask,
59+
block_size=block_size,
60+
dpace_alpha=dpace_alpha,
61+
)
62+
else:
63+
decay_fn = partial(dflash_loss_decay, gamma=gamma)
64+
5065
loss, term_losses = compound_loss(
5166
logits,
5267
targets,
5368
loss_mask,
5469
pos_idx,
5570
loss_config=loss_config,
56-
decay_fn=partial(dflash_loss_decay, gamma=gamma),
71+
decay_fn=decay_fn,
5772
)
5873

5974
pred_ids = torch.argmax(logits, dim=-1)

src/speculators/models/dspark/core.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,11 +84,17 @@ def get_trainer_kwargs(**kwargs) -> tuple[dict, dict]:
8484
gamma = kwargs.get("dflash_decay_gamma", 4.0)
8585
max_anchors = kwargs.get("max_anchors", 3072)
8686
confidence_head_alpha = kwargs.get("confidence_head_alpha", 1.0)
87+
per_position_loss_weight = kwargs.get(
88+
"per_position_loss_weight", "fixed-exp-decay"
89+
)
90+
dpace_alpha = kwargs.get("dpace_alpha", 0.5)
8791
shared = {
8892
"loss_config": loss_config,
8993
"gamma": gamma,
9094
"max_anchors": max_anchors,
9195
"confidence_head_alpha": confidence_head_alpha,
96+
"per_position_loss_weight": per_position_loss_weight,
97+
"dpace_alpha": dpace_alpha,
9298
}
9399
return dict(shared), dict(shared)
94100

@@ -105,6 +111,8 @@ def forward(
105111
gamma: float = 4.0,
106112
max_anchors: int = 3072,
107113
confidence_head_alpha: float = 1.0,
114+
per_position_loss_weight: str = "fixed-exp-decay",
115+
dpace_alpha: float = 0.5,
108116
**kwargs,
109117
):
110118
hidden, logits, targets, aligned_loss_mask, anchored_block_indices = (
@@ -167,6 +175,8 @@ def forward(
167175
loss_config=loss_config or _DEFAULT_LOSS_CONFIG,
168176
gamma=gamma,
169177
confidence_head_alpha=confidence_head_alpha,
178+
per_position_loss_weight=per_position_loss_weight,
179+
dpace_alpha=dpace_alpha,
170180
)
171181
draft_tokens = torch.argmax(logits, dim=-1)
172182
return draft_tokens, loss, metrics

src/speculators/models/dspark/metrics.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
compound_loss,
1919
compute_accuracy_multi_step,
2020
dflash_loss_decay,
21+
dpace_loss_decay,
2122
)
2223

2324
__all__ = [
@@ -31,13 +32,15 @@ def _masked_decayed_mean(
3132
elementwise: torch.Tensor, # [1, T]
3233
loss_mask: torch.Tensor, # [1, T]
3334
pos_idx: torch.Tensor, # [1, T]
34-
decay_fn: Callable[[torch.Tensor], torch.Tensor] | None,
35+
decay_fn: Callable[..., torch.Tensor] | None,
3536
) -> torch.Tensor:
3637
"""Masked, optionally position-decayed mean of a precomputed per-position term."""
3738
loss_mask = loss_mask.to(elementwise.dtype)
3839
weighted = elementwise * loss_mask
3940
if decay_fn is not None:
40-
weighted = weighted * decay_fn(pos_idx.to(weighted.dtype))
41+
weighted = weighted * decay_fn(
42+
pos_idx.to(weighted.dtype), elementwise_loss=elementwise
43+
)
4144
denominator = loss_mask.sum(dim=1) + _EPS
4245
return (weighted.sum(dim=1) / denominator).mean()
4346

@@ -51,13 +54,23 @@ def compute_metrics(
5154
loss_config: LossConfig,
5255
gamma: float = 4.0,
5356
confidence_head_alpha: float = 1.0,
57+
per_position_loss_weight: str = "fixed-exp-decay",
58+
dpace_alpha: float = 0.5,
5459
) -> tuple[torch.Tensor, dict]:
5560
"""Compute the DSpark loss and a metrics dict (``*_sum``/``*_total`` pairs)."""
5661

5762
device = logits.device
5863
seq_len = logits.shape[1]
5964
pos_idx = (torch.arange(seq_len, device=device) % block_size).unsqueeze(0)
60-
decay_fn = partial(dflash_loss_decay, gamma=gamma)
65+
if per_position_loss_weight == "dpace":
66+
decay_fn = partial(
67+
dpace_loss_decay,
68+
loss_mask=loss_mask,
69+
block_size=block_size,
70+
dpace_alpha=dpace_alpha,
71+
)
72+
else:
73+
decay_fn = partial(dflash_loss_decay, gamma=gamma)
6174

6275
loss, term_losses = compound_loss(
6376
logits, targets, loss_mask, pos_idx, loss_config=loss_config, decay_fn=decay_fn

src/speculators/models/metrics.py

Lines changed: 64 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@ def lk_hybrid_loss(
244244
return elementwise_loss # noqa: RET504
245245

246246

247-
def dflash_loss_decay(pos_idx: torch.Tensor, gamma: float):
247+
def dflash_loss_decay(pos_idx: torch.Tensor, gamma: float, **_kwargs):
248248
"""Compute DFlash-style exponential decay weights per position.
249249
250250
Position 0 gets weight 0, position 1 gets weight 1, and subsequent positions
@@ -265,7 +265,7 @@ def dflash_loss_decay(pos_idx: torch.Tensor, gamma: float):
265265
return decay_mult # noqa: RET504
266266

267267

268-
def exp_loss_decay(pos_idx: torch.Tensor, gamma: float):
268+
def exp_loss_decay(pos_idx: torch.Tensor, gamma: float, **_kwargs):
269269
"""Compute simple exponential decay weights as gamma^pos_idx.
270270
271271
Args:
@@ -278,6 +278,57 @@ def exp_loss_decay(pos_idx: torch.Tensor, gamma: float):
278278
return gamma**pos_idx
279279

280280

281+
def dpace_loss_decay(
282+
pos_idx: torch.Tensor, # noqa: ARG001
283+
loss_mask: torch.Tensor,
284+
block_size: int,
285+
dpace_alpha: float,
286+
elementwise_loss: torch.Tensor,
287+
**_kwargs,
288+
):
289+
"""
290+
Per-position block-drafting loss weight based on D-PACE
291+
292+
Args:
293+
elementwise_loss: requires to be cross-entropy loss, negative log-likelihood
294+
of per-position confidence
295+
dpace_alpha: confidence smoothing constant
296+
297+
Returns:
298+
Decay multiplier tensor with same shape as pos_idx.
299+
"""
300+
with torch.no_grad():
301+
# convert CE to per-position confidence
302+
q = torch.exp(-elementwise_loss).float()
303+
304+
# reshape loss to [num_anchors, block_size]
305+
# for intra-block cumulative multiplication
306+
if q.shape[1] % block_size != 0:
307+
raise ValueError(
308+
f"q.shape[1] ({q.shape[1]}) must be divisible by "
309+
f"block_size ({block_size})"
310+
)
311+
num_anchors = q.shape[1] // block_size
312+
q = q.reshape(num_anchors, block_size)
313+
mask = loss_mask.reshape(num_anchors, block_size).to(q.dtype)
314+
315+
# smoothed confidence for numerical stability
316+
smooth = (1.0 - dpace_alpha) * q + dpace_alpha
317+
smooth = torch.where(mask > 0, smooth, torch.ones_like(smooth))
318+
319+
# prefix cumulative production
320+
prefix = torch.cumprod(smooth, dim=-1)
321+
322+
# suffix summation: flip -> cumsum -> flip
323+
weight = torch.flip(
324+
torch.cumsum(torch.flip(prefix * mask, dims=[-1]), dim=-1), dims=[-1]
325+
)
326+
weight = weight * mask
327+
328+
# reshape weight
329+
return weight.reshape(1, -1)
330+
331+
281332
_LOSS_FN_MAP: dict[str, Callable[[torch.Tensor, torch.Tensor], torch.Tensor]] = {
282333
"kl_div": kl_div_loss,
283334
"rkl": reverse_kl_div_loss,
@@ -360,7 +411,7 @@ def compound_loss(
360411
loss_mask: torch.Tensor,
361412
pos_idx: torch.Tensor,
362413
loss_config: LossConfig,
363-
decay_fn: Callable[[torch.Tensor], torch.Tensor] | None = None,
414+
decay_fn: Callable[..., torch.Tensor] | None = None,
364415
) -> tuple[torch.Tensor, dict[str, torch.Tensor]]:
365416
"""Compute a weighted sum of loss terms.
366417
@@ -377,7 +428,12 @@ def compound_loss(
377428
multi = len(loss_config) > 1
378429
for name, (fn, weight) in loss_config.items():
379430
term = loss_function(
380-
logits, targets, loss_mask, pos_idx, loss_fn=fn, decay_fn=decay_fn
431+
logits,
432+
targets,
433+
loss_mask,
434+
pos_idx,
435+
loss_fn=fn,
436+
decay_fn=decay_fn,
381437
)
382438
if multi:
383439
term_losses[f"{name}_loss"] = term.detach()
@@ -391,7 +447,7 @@ def loss_function(
391447
loss_mask: torch.Tensor, # shape: [1, seq_len]
392448
pos_idx: torch.Tensor, # shape: [1, seq_len]
393449
loss_fn: Callable[[torch.Tensor, torch.Tensor], torch.Tensor] = kl_div_loss,
394-
decay_fn: Callable[[torch.Tensor], torch.Tensor] | None = None,
450+
decay_fn: Callable[..., torch.Tensor] | None = None,
395451
):
396452
"""Compute masked, optionally position-decayed training loss.
397453
@@ -412,7 +468,9 @@ def loss_function(
412468
elementwise_loss = elementwise_loss * loss_mask
413469

414470
if decay_fn is not None:
415-
decay_mult = decay_fn(pos_idx.to(elementwise_loss.dtype))
471+
decay_mult = decay_fn(
472+
pos_idx.to(elementwise_loss.dtype), elementwise_loss=elementwise_loss
473+
)
416474
elementwise_loss = elementwise_loss * decay_mult
417475

418476
denominator = loss_mask.sum(dim=1) + _EPS

0 commit comments

Comments
 (0)