---
title: 基于昇腾使用speculators训练DSpark投机推理草稿模型-官方技术文章-昇腾社区
description: 在华为 Ascend NPU 上从零训练 DSpark draft 模型&#xff0c;并接入 vLLM-Ascend 进行投机解码推理
keywords: DSpark,基于昇腾使用,训练,投机推理草稿,模型,官方技术文章,昇腾社区,投机推理介绍
url: https://www.hiascend.com/developer/techArticles/20260720-10
section: (其他)
---

# 基于昇腾使用speculators训练DSpark投机推理草稿模型-官方技术文章-昇腾社区

URL: https://www.hiascend.com/developer/techArticles/20260720-10
描述: 在华为 Ascend NPU 上从零训练 DSpark draft 模型&#xff0c;并接入 vLLM-Ascend 进行投机解码推理
关键词: DSpark,基于昇腾使用,训练,投机推理草稿,模型,官方技术文章,昇腾社区,投机推理介绍

官方技术文章 [了解详情](https://www.hiascend.com/zh/developer/techArticles)

基于昇腾使用speculators训练DSpark投机推理草稿模型

基于昇腾使用speculators训练DSpark投机推理草稿模型

模型训练训练

发表于: 2026/07/20

38

0

# 基于昇腾使用speculators训练DSpark投机推理草稿模型

目标读者：在华为 Ascend NPU 上从零训练 DSpark draft 模型并接入 vLLM-Ascend 投机解码。实跑参考：Qwen3-30B-A3B verifier + DSpark draft，使用UltraChat数据集。

## 1. DSpark 投机推理介绍

### 1.1 投机解码（Speculative Decoding）原理

投机解码是一种无损加速推理的技术。其核心思想是：用一个轻量级的草稿模型（Draft Model）快速生成多个候选 token，然后由验证模型（Verifier，即原始大模型）在一次前向传播中同时验证所有候选 token，接受匹配的 token 并拒绝不匹配的 token。

工作流程：

auth

1

2

3

4

5

6

1. 草稿模型自回归生成 K 个候选 token: [t1, t2, ..., tK]

2. 验证模型一次前向传播验证所有候选 token

3. 从 t1 开始逐个比对：

- 若 t_i 与验证模型贪心结果一致 → 接受，继续验证 t_{i+1}

- 若 t_i 不一致 → 拒绝 t_i 及后续所有 token，但保留验证模型在 t_i位置的正确输出（bonus token）

4. 接受长度 = 1（bonus token）+ 连续匹配的 token 数

1. 草稿模型自回归生成 K 个候选 token: [t1, t2, ..., tK]
2. 验证模型一次前向传播验证所有候选 token
3. 从 t1 开始逐个比对：
   - 若 t_i 与验证模型贪心结果一致 → 接受，继续验证 t_{i+1}
   - 若 t_i 不一致 → 拒绝 t_i 及后续所有 token，但保留验证模型在 t_i 位置的正确输出（bonus token）
4. 接受长度 = 1（bonus token）+ 连续匹配的 token 数

关键指标：

●

接受率（Acceptance Rate）：被验证模型接受的 token 占草稿模型提议 token 的比例

●

接受长度（Acceptance Length）：平均每次投机验证后接受的 token 总数，直接决定加速比

### 1.2 DFlash —— 块级草稿模型

DFlash 是一种基于锚点的块级草稿生成方法，其核心设计是：

●

锚点（Anchor）：在序列中选取若干位置作为锚点，每个锚点对应一个预测块

●

块预测（Block Drafting）：每个锚点同时预测`block_size`个 token，构成一个预测块

●

交叉注意力（Cross-Attention）：草稿模型的 Query 来自预测块，Key/Value 来自验证模型的前缀上下文 + 预测块自身

●

多层级隐状态（Aux Hidden States）：从验证模型的指定层提取隐状态，经线性投影后作为草稿模型的上下文输入

### 1.3 DSpark —— DFlash + Markov + Confidence

DSpark 在 DFlash 的基础上增加了两个关键模块，形成"DFlash 骨干 + Markov 偏置头 + 置信度头"的三段式架构：

#### 1.3.1 Markov 头（马尔可夫偏置头）

Markov 头通过低秩分解建模 token 间的序列依赖关系，对骨干 logits 施加偏置修正。

DFlash 骨干的预测块内各位置是独立生成的，缺乏 token 间的序列依赖建模。Markov 头通过低秩偏置补充了这种依赖关系，使得后续位置的预测能利用前序 token 的信息，显著提高多步预测的准确率。

#### 1.3.2 置信度头（Confidence Head）

置信度头预测每个位置的接受概率，用于推理时动态调整投机长度：

●

输入：`hidden_states`（可选拼接 Markov 嵌入）

●

输出：每个预测位置的标量 logit，经 sigmoid 后表示接受概率

●

训练目标：BCE loss，标签为解析接受率`1 - d_TV`（draft 与 target 分布的交叠度）

置信度头的作用：

●

训练时作为辅助损失，引导模型产生与验证器分布更接近的输出

●

推理时可用于动态决定投机长度（高置信度 → 多投机，低置信度 → 少投机）

#### 1.3.3 DSpark 完整前向流程

输入: input_ids, hidden_states, verifier_hidden_states, loss_mask

1. 骨干前向（predict_next_token=True）:
   ├── 选取锚点，构建注意力掩码（include_anchor=True）
   ├── 构建 mask_token_ids（锚点位置填真实 token id，其余填 mask_token_id）
   ├── embed → 预测块隐状态
   ├── fc + hidden_norm → 上下文隐状态
   ├── 解码器层（交叉注意力：Q=预测块, KV=上下文+预测块）
   └── norm + lm_head → 骨干 logits

2. Markov 头:
   ├── prev_embeddings(prev_token_ids) → prev_emb
   ├── block_bias(prev_token_ids, hidden, prev_emb) → markov_bias
   └── logits = logits + markov_bias

3. 置信度头:
   ├── confidence_features = cat([hidden, prev_emb]) 或 hidden
   └── confidence_logits = confidence_head(confidence_features)

4. 损失计算:
   ├── CE/TV 复合损失（logits vs targets）
   ├── 置信度 BCE 损失
   └── 加权求和

## 2. 训练环境

### 2.1 硬件

●

NPU：Ascend 910B3（实测环境），推荐 4 卡+（TP=2 跑 30B verifier，TP=2跑训练）。

●

显存：30B MoE verifier + draft，bf16，TP=2，`gpu_memory_utilization=0.85`。

### 2.2 软件（实测版本）

| 组件 | 版本 |
| --- | ---|
| CANN / torch_npu | torch_npu2.10.0（torch 2.10.0+cpu） |
| vLLM | 0.23.1rc1.dev752 |
| vLLM-Ascend | 装在/vllm-workspace/vllm-ascend（vllm_ascendplugin） |
| transformers | 5.5.4 |
| speculators | 0.7.0.dev88（editable，见 §4） |
| Python | 3.11 |

### 2.3 模型 / 数据

●

Verifier：`/mnt/cephfs/models/Qwen3-30B-A3B/Qwen/Qwen3-30B-A3B`（Qwen3 MoE）。

●

训练数据：`/data/path/pretrain/ultrachat-7k-30B-regen-thinking`（UltraChat 再生，带 verifier hidden state）。

## 3. speculators 工具介绍

speculators 是 Red Hat 维护的 draft 模型训练 + 转换 + 评测工具集，支持 EAGLE/EAGLE3/MTP/DFlash/DSpark 等多种 speculator。关键目录：

scripts/
  train.py                 # 训练入口（本文重点）
  launch_vllm.py           # 一键起 vLLM + 评测
  evaluate/                # 评测脚本
  prepare_data.py          # 数据准备（生成 verifier hidden state）
  data_generation_offline.py
  build_vocab_mapping.py
src/speculators/
  models/
    dflash/                # DFlash 基类（backbone / attention / utils）
    dspark/                # DSpark = DFlash + markov + confidence
      core.py              # draft forward（prev_token 在此，见 §5.2）
      metrics.py           # loss + 指标（accept_len slot0 在此，见 §5.2(d)）
      model_definitions.py # MarkovHead / ConfidenceHead
    metrics.py             # 通用 loss_function（decay / 分母）
  train/trainer.py         # 训练循环（_StepTimer 在此，见 §5.2(b)）

editable 安装意味着`import speculators`直接读这个目录的源码——所以修改代码就是改这里的文件，无需重装。

## 4. 安装过程

cd /data/path/speculators_new/speculators

# editable 安装：改源码即时生效（关键）
pip install -e .

# 验证
python -c "import speculators, os; print(os.path.dirname(speculators.__file__))"

pip show speculators | grep -i editable

前置依赖（torch_npu / vllm-ascend）按机器已有环境为准，本文不重述。

## 5. 代码修改说明

### 5.1 为什么需要修改

speculators仓库代码支持Dspark训练，需做 4 项代码修改。其中3个已经通过PR合入。

### 5.2 修改说明

#### (a) pos0 decay 调整（必做）✅已合入 PR#798

改什么：DSpark 默认`sample_from_anchor=True`，slot 0 是第一个真实预测（不是 anchor）；但原 loss 的 position decay 仍按 DFlash 约定算（pos0 权重 0、当 anchor 排除），导致 pos0 几乎不参与训练——而 pos0 是接受率最高的位置。本修改给 decay 加一个`sample_from_anchor`开关，让 pos0 拿到全权重。

怎么改（`metrics.py::dflash_loss_decay`加`sample_from_anchor`参数）：

●

`sample_from_anchor=False`（DFlash）：pos0=anchor 权重 0，`exp(-(pos-1)/gamma)`（不变）。

●

`sample_from_anchor=True`（DSpark）：pos0=真实预测、权重 1.0（`exp(-pos/gamma)`，pos0=exp(0)=1），后续位置衰减。

`dflash/metrics.py`和`dspark/metrics.py`各把`sample_from_anchor`传进`decay_fn`。

纯 pos0 decay 调整，不含weighted-mean 分母改动（那是我们试过又撤掉的另一个修改，别用）。对 main 三个文件 blob 完全匹配，clean apply。

#### (b) NPU 设备适配（必做）⏳待合入 PR#819

speculators 训练入口和 trainer 默认用`torch.cuda`，在 NPU 上会报错或无效。本修改：

| 文件 | 改动 |
| --- | ---|
| scripts/train.py | torch.cuda.manual_seed_all→torch.npu.manual_seed_all；torch.cuda.is_available()/empty_cache→torch.npu.* |
| src/speculators/train/trainer.py | _StepTimer的torch.cuda.synchronize()改用torch.accelerator.synchronize()（通用加速器接口，NPU/CUDA 均支持），并保留torch.cuda.synchronize()做 fallback |

#### (c) markov prev_token 写法调整（必做）✅已合入 PR#806

改什么：`dspark/core.py`中 markov head 的`prev_token_ids`，原代码两种模式都做 shift 一格；改为按`sample_from_anchor`区分——DSpark 直接用`block_tokens`，DFlash 才 shift。因为偏置输入须与推理 markov 链一致，否则训练学到的偏置在推理时错位。

怎么改（按`sample_from_anchor`分支，DSpark 不 shift、DFlash 才 shift）：

if self.config.sample_from_anchor:        # DSpark 默认：prev = block_tokens[k]
    prev_token_ids = block_tokens
else:                                      # DFlash (即修改前）：prev = block_tokens[k-1]
    prev_token_ids = torch.cat([block_tokens[:, :1], block_tokens[:, :-1]], dim=1)

举例：DSpark，`block_size=8`，`block_tokens = [A, B, C, D, E, F, G, H]`（A 是 anchor）。slot k 预测`block_tokens[k+1]`，正确的前一个 token 是`block_tokens[k]`，即`prev_token_ids`应等于`block_tokens`本身。

|  | prev_token_ids实际取值 | slot 1 处 |
| --- | --- | ---|
| block_tokens（正确） | [A, B, C, D, E, F, G, H] | B ✅ |
| 改前（shift，错） | [A, A, B, C, D, E, F, G] | A ❌（差一格） |
| 改后（不 shift，对） | [A, B, C, D, E, F, G, H] | B ✅ |

改前的 shift 把数组整体右移一格、开头补 A，从 slot 1 起每个 slot 的「前一个 token」都错位成了再前一个。改后在 DSpark 下直接用`block_tokens`，与推理 markov 链一致。

影响范围：仅 pos1+（slot 0 改前改后都是 A），不涉及 pos0；修改后需从头重训。

#### (d) slot0 指标口径 ⏳待合入 PR#805

`dspark/metrics.py`里`draft_mask`/`accept_prefix`/`conf_prefix`原本硬编码`[:, 1:]`（DFlash 的 slot0-skip），对 DSpark（slot0 是真实预测）少算了 slot0→ 报出的`accept_len`/`confidence_cumprod_bias`偏低。改成`[:, start_pos:]`（`start_pos = 0 if sample_from_anchor else 1`）。

## 6. 训练详细操作

### 6.1 数据准备

DSSpark 训练数据 = tokenized 序列 + loss_mask + verifier hidden states。其中前两者是一次性离线生成的静态文件；hidden states 有「在线 / 离线」两条路径，本实测走在线（hidden state 由一个独立 vLLM 服务在训练时按需生成）。

数据分三个阶段准备：

| 阶段 | 产出 | 是否含 hidden state | 谁来做 |
| --- | --- | --- | ---|
| 阶段 0response regeneration | 原始对话 jsonl（assistant 回复用 verifier 重生成） | ❌ | scripts/response_regeneration/script.py（在线、NPU vLLM chat 服务） |
| 阶段 1tokenize + mask | Arrow(input_ids/loss_mask/seq_len) +token_freq.pt | ❌ | scripts/prepare_data.py（离线、CPU） |
| 阶段 2hidden state 生成 | 每条样本各层 hidden state 张量 | ✅ | 在线：独立 vLLM 服务（本路径）；或离线：data_generation_offline.py |

#### 6.1.0 阶段 0：用 verifier 重生成 UltraChat 回复（response regeneration）

`prepare_data.py`（§6.1.1）的输入是「原始对话 jsonl」。但原始 UltraChat 的 assistant 回复是别的模型生成的，分布和 verifier 不一致——DSpark draft 必须学 verifier 的分布，所以要先用 verifier 把 assistant 回复重新生成一遍（on-policy regeneration）。这一步产出 §6.1.1 要的 jsonl。

第 1 步：起一个 vLLM chat 服务（verifier 当 normal chat 模型 serve）

cat > /data/path/datasets/var/start_regenerate_service.sh <<'EOF'
#!/bin/bash
set -euo pipefail
export TMPDIR=/data/path/datasets/tmp
export ASCEND_RT_VISIBLE_DEVICES=6,7        # NPU 用 ASCEND_RT_VISIBLE_DEVICES，不是 CUDA
export HCCL_SOCKET_IFNAME=lo
export GLOO_SOCKET_IFNAME=lo
export HCCL_NPU_SOCKET_PORT_RANGE=18500-18749
export HCCL_OP_EXPANSION_MODE=AIV

exec python -m vllm.entrypoints.openai.api_server \
    --model /mnt/cephfs/models/Qwen3-30B-A3B/Qwen/Qwen3-30B-A3B \
    --served-model-name qwen3-30b-a3b \
    --tensor-parallel-size 2 \
    --port 8005 \
    --max-model-len 16384 \
    --trust-remote-code \
    --gpu-memory-utilization 0.9 \
    --seed 42
EOF
chmod +x /data/path/datasets/var/start_regenerate_service.sh
nohup /data/path/datasets/var/start_regenerate_service.sh > /tmp/regen_service.log 2>&1 &

要点：

●

不加`--chat-template`→ 用 Qwen3默认 template（thinking mode），回复带`<think>...</think>`。regenerate / train / bench 三处是否开启思考模式需保持一致。

●

模型必须和训练 verifier 一致（这里 30B-A3B）。不要拿 8B 生成、再训 30B draft（分布错配）。

●

等`curl -sf http://localhost:8005/health`通了再下一步（30B 首次启动 ~150–250s）。

第 2 步：跑 regeneration

cd /data/path/speculators_new/speculators
python scripts/response_regeneration/script.py \
  --dataset ultrachat \
  --endpoint http://localhost:8005/v1/chat/completions \
  --model qwen3-30b-a3b \
  --limit 20000 \
  --concurrency 64 \
  --max-tokens 8192 \
  --resume \
  --outfile /data/path/pretrain/ultrachat-20k-regen-thinking-qwen3-30b-a3b.jsonl

flag 说明：

| flag | 含义 |
| --- | ---|
| --dataset | ultrachat/magpie/gsm8k（HF Hub） |
| --endpoint | 指向上面起的 chat 服务 |
| --model | 服务--served-model-name（省略则脚本自动探测） |
| --limit | 生成 N 条后停（UltraChat 200k 全量太大，按需取子集） |
| --concurrency | 客户端并发。30B 在 TP2 上 ~64 就把 decode 算力打满了（compute-bound），加到 96/128 不提速、只多吃 KV cache |
| --max-tokens | 单条回复上限（thinking 长，8192） |
| --resume | 断点续跑：跳过 outfile 里已生成的行（按metadata.idx去重） |

脚本逻辑：读数据集 → 保留每条的 user 轮、丢弃原 assistant 轮→ 把 prefix 发给 chat 服务重新生成 assistant 回复 → 多轮对话逐轮把生成结果拼回 prefix 再生成下一轮。产出 jsonl 每行：`{id, conversations:[{from:human/gpt,value}, ...], metadata:{idx, finish_reasons, usage}}`。

吞吐参考：Qwen3-30B-A3B TP2、concurrency 64、thinking：约 15 条/分。2 万条约 22 小时。要更快只能加卡（多个 TP2 服务分片并行），不是加客户端并发。

对齐（关键）：regenerate 的 模型 + template 必须和 train/bench 一致：

| 阶段 | 模型 | template |
| --- | --- | ---|
| regenerate（本节） | verifier（30B-A3B） | 默认 thinking |
| train（§6.2） | 同 verifier | 同 thinking（prepare_data.py用 verifier tokenizer） |
| bench（§8） | 同 verifier + draft | 同 thinking（vLLM 别把--chat-template改成 no_thinking） |

#### 6.1.1 阶段 1：tokenize + loss_mask

把原始对话（UltraChat 等聊天数据）按 verifier 的 chat template tokenize，并生成`loss_mask`（标记哪些 token 是要学的「assistant 回复」，user 部分置 False）。这一步只需要 CPU，不需要 NPU/GPU。

cd /data/path/speculators_new/speculators

python scripts/prepare_data.py \
  --model /mnt/cephfs/models/Qwen3-30B-A3B/Qwen/Qwen3-30B-A3B/ \
  --data /data/path/pretrain/ultrachat-20k-regen-thinking-qwen3-30b-a3b.jsonl \
  --seq-length 8192 \
  --output /data/path/pretrain/ultrachat-7k-30B-regen-thinking \
  --num-preprocessing-workers 8 \
  --seed 42 --trust-remote-code

产出目录（本环境实测，`dataset_info.json`）：

/data/path/pretrain/ultrachat-7k-30B-regen-thinking/
  data-00000-of-00001.arrow   # 7790 条样本
  dataset_info.json           # 字段定义
  state.json
  token_freq.pt               # token 频率（loss 归一化/采样用）

Arrow 里每条样本 3 个字段：

| 字段 | dtype | 含义 |
| --- | --- | ---|
| input_ids | int32List | tokenize 后的 token id 序列 |
| loss_mask | boolList | 哪些位置参与训练（assistant 回复=True） |
| seq_len | int64 | 序列实际长度（用于切 anchor/对齐） |

注意：这个 Arrow 不含 hidden state。draft cross-attention 要的 verifier hidden state 由阶段 2 提供。

#### 6.1.2 阶段 2 在线生成hidden-state （本实测路径）

原理：单独起一个 vLLM 服务，把 verifier 当 normal chat 模型 serve，但用`extract_hidden_states`这个 speculative method +`ExampleHiddenStatesConnector`（角色`kv_producer`），让它在处理每条请求时把指定层的 hidden state 缓存写到共享目录（`/tmp/hidden_states`）。训练进程的 dataloader 发现某条样本没有 cached hidden state 时，就向这个服务发请求触发生成，生成完读取、用完即删。

第 1 步：起 hidden-state 生成服务

用 speculators 自带的`scripts/launch_vllm.py`（它会自动拼好`--speculative_config`（`extract_hidden_states`）和`--kv_transfer_config`（`ExampleHiddenStatesConnector`/`kv_producer`），不用手写 JSON）。命令格式：

ASCEND_RT_VISIBLE_DEVICES=0,1 \
nohup python scripts/launch_vllm.py \
  /mnt/cephfs/models/Qwen3-30B-A3B/Qwen/Qwen3-30B-A3B/ \
  --target-layer-ids 1 12 23 34 45 \
  --tensor-parallel-size 2 \
  --trust-remote-code \
  --port 9736 \
  --compilation-config '{"cudagraph_capture_sizes":[2,4,8,16],"cudagraph_mode":"PIECEWISE"}' \
  --seed 42 \
  > /tmp/hidden_states_server.log 2>&1 &

要点：

●

`--target-layer-ids 1 12 23 34 45`必须和训练侧的`--target-layer-ids`完全一致（draft cross-attention 取的 5 层）。`launch_vllm.py`默认`--include-last-layer`，会自动把末层（Qwen3-30B 的第 48 层）追加进去，所以服务最终导出的`eagle_aux_hidden_state_layer_ids = [1, 12, 23, 34, 45, 48]`——末层用于算 target logits。

●

`--hidden-states-path`（默认`/tmp/hidden_states`）= hidden state 落盘目录（`shared_storage_path`）。训练进程必须能访问到同一个目录（本机同节点，直接共用`/tmp`）。

●

`--`之后全是透传给 vLLM 的参数（`--tensor-parallel-size`/`--trust-remote-code`/`--port`/`--compilation-config`/`--seed`等）；`launch_vllm.py`还会自动补`--no-enable-chunked-prefill`。

●

NPU 用`ASCEND_RT_VISIBLE_DEVICES`指定卡。

●

用`--dry-run`可以先打印它拼出来的完整命令、不实际起服务，方便核对。

第 2 步：训练时配 3 个 flag 指向它（见 §6.2 完整命令里的这几行）：

--vllm-endpoint http://localhost:9736/v1 \   # 指向上面的服务
  --on-missing generate \                       # 样本缺 hidden state 时，向服务请求在线生成
  --on-generate delete                          # 生成、读取、用完即删（不落盘常驻）

flag 含义（来自`scripts/train.py`参数定义）：

| flag | 取值 | 含义 |
| --- | --- | ---|
| --vllm-endpoint | http://localhost:9736/v1 | hidden-state 生成服务地址。仅当--on-missing=generate且确有缺失样本时才需要。 |
| --on-missing | generate | 样本没有 cached hidden state 时的行为：generate=在线生成（本路径）；skip=跳过；warn=跳过并警告；raise=报错。 |
| --on-generate | delete | 新生成的 hidden state 用完怎么办：delete=用完即删（纯在线，省盘，本路径）；cache=存进--hidden-states-path（可后续 epoch 复用 → 混合在线/离线）。 |

因为`--on-generate delete`，`/tmp/hidden_states`在训练过程中是「生成→消费→删除」的滚动缓存，训练结束后基本是空的；这正是「在线生成」的体现——hidden state 不预先落盘，每个 epoch 都现算。

#### 6.1.3 阶段 2 离线生成hidden-state（备选）

如果想把 hidden state 预先全量生成存盘（避免训练时还要占一个 verifier 推理服务、或要复用多轮 epoch），用离线路径：跑`scripts/data_generation_offline.py`把每条样本各层 hidden state 写进`--hidden-states-path`（默认`data_path/hidden_states/`），训练时`--on-missing raise`（或`warn`）即可，不再需要`--vllm-endpoint`。本实测没走这条，仅列出作为备选。

如果想把 hidden state 预先全量生成存盘（避免训练时还要占一个 verifier 推理服务、或要复用多轮 epoch），用离线路径：跑`scripts/data_generation_offline.py`把每条样本各层 hidden state 写进`--hidden-states-path`（默认`data_path/hidden_states/`），训练时`--on-missing raise`（或`warn`）即可，不再需要`--vllm-endpoint`。本实测没走这条，仅列出作为备选。

#### 6.1.4 在线 vs 离线怎么选

| 维度 | 在线（本路径） | 离线 |
| --- | --- | ---|
| 磁盘 | 几乎不占（用完即删） | 占一份完整 hidden state（30B × 6 层 × 7790 条，较大） |
| 训练时资源 | 多占一个 verifier 推理服务（端口 9736） | 不占 |
| 多 epoch | 每 epoch 都重算 hidden state | 第 2+ epoch 直接读盘 |
| 适合 | 单次/少量 epoch、磁盘紧、数据会变 | 多 epoch、要复用、想固定数据 |

本实测 2 epoch、磁盘优先，选在线。若要训多 epoch 想省 verifier 重算，可把`--on-generate`改成`cache`（第 1 epoch 在线生成并落盘，之后读盘）。

### 6.2 训练命令

ASCEND_RT_VISIBLE_DEVICES=0,1 torchrun --nproc_per_node=2 scripts/train.py \
  --verifier-name-or-path /mnt/cephfs/models/Qwen3-30B-A3B/Qwen/Qwen3-30B-A3B/ \
  --data-path /data/path/pretrain/ultrachat-7k-30B-regen-thinking \
  --save-path ../output/dspark_qwen3_30b_ultrachat_7k_final_test/checkpoints \
  --vllm-endpoint http://localhost:9736/v1 \
  --draft-attn-impl sdpa --total-seq-len 8192 \
  --speculator-type dspark \
  --block-size 8 --max-anchors 256 \
  --target-layer-ids 1 12 23 34 45 \
  --epochs 2 --checkpoint-freq 1 --lr 3e-4 \
  --on-missing generate --on-generate delete --seed 42 \
  --num-layers 5 --draft-arch qwen3 --mask-token-id 151669 \
  --markov-rank 256 --markov-head-type vanilla \
  --enable-confidence-head --confidence-head-with-markov \
  --loss-fn '{"ce": 0.7, "tv": 0.3}' --confidence-head-alpha 1.0 \
  --full-attention-indices 0 1 2 3 4 \
  --logger tensorboard \
  --log-dir ../output/dspark_qwen3_30b_ultrachat_7k_final_test/logs

### 6.3 关键参数解释

| 参数 | 值 | 说明 |
| --- | --- | ---|
| --speculator-type dspark | dspark | 选 DSpark |
| --block-size 8 | 8 | 每个 anchor 预测 8 个 token（推理num_speculative_tokens=7） |
| --num-layers 5 | 5 | draft decoder 层数（轻量） |
| --draft-arch qwen3 | qwen3 | draft 用 Qwen3 decoder |
| --target-layer-ids 1 12 23 34 45 | — | 从 verifier 哪些层取 hidden state |
| --markov-rank 256 | 256 | markov head 低秩维度 |
| --markov-head-type vanilla | vanilla | markov head 类型 |
| --enable-confidence-head | — | 开 confidence head（训练用） |
| --loss-fn {"ce":0.7,"tv":0.3} | — | TV-dominant 复合 loss |
| --full-attention-indices 0 1 2 3 4 | 0..num_layers-1 | ⚠️ 指定哪几层用 full attention，没列的层默认走 sliding window。--num-layers 5必须配--full-attention-indices 0 1 2 3 4（全部 5 层，index 从 0 起）。改--num-layers时要同步把--full-attention-indices改成0 .. num_layers-1。 |
| --total-seq-len 8192 | 8192 | 序列长度 |

### 6.4 产出

../output/dspark_qwen3_30b_ultrachat_7k_final_test/
  checkpoints/
    0/  1/                # 每 epoch 一个（checkpoint-freq 1）
    checkpoint_best -> 1
    epoch0_end -> 0
    epoch1_end -> 1
    train_command.txt     # 含完整命令 + git SHA（可复现）
  logs/                   # tensorboard
  train.log

每个 checkpoint 目录里有`model.safetensors`+`config.json`，`config.json`含`block_size=8, markov_rank=256, sample_from_anchor=true`等。

## 7. 训练结果分析

### 7.1 训练侧指标（teacher-forced）

每个 epoch 结束的valeval（`train.log`，`val/*_epoch`），2 epoch 完整对比：

| 指标 | epoch 0 | epoch 1（最终） |
| --- | --- | ---|
| val/accept_len_epoch | 1.834 | 2.067↑ |
| val/accept_rate_epoch | 0.277 | 0.328 |
| val/full_acc_epoch | 0.336 | 0.404 |
| val/loss_epoch | — | 1.366（ce 1.480 / tv 0.303） |
| pos0 acc | 0.494 | 0.575 |
| pos1 acc | 0.402 | 0.481 |
| pos2 acc | 0.355 | 0.427 |
| pos3 acc | 0.323 | 0.390 |
| pos4 acc | 0.300 | 0.363 |
| pos5 acc | 0.284 | 0.346 |
| pos6 acc | 0.271 | 0.332 |
| pos7 acc | 0.258 | 0.317 |

最终 epoch 的 per-position：`0.575 → 0.481 → 0.427 → 0.390 → 0.363 → 0.346 → 0.332 → 0.317`。

## 8. vLLM bench 结果

### 8.1 部署 + 评测

用 vLLM-Ascend 加载 verifier + DSpark draft（`speculative_config method=dspark`），再用`vllm bench serve`打 UltraChat 流量。

核心部署参数（来自 bench 日志）：

vllm serve /mnt/cephfs/models/Qwen3-30B-A3B/Qwen/Qwen3-30B-A3B \
  --port 8014 --served-model-name qwen3-30b-a3b \
  --tensor-parallel-size 2 --gpu-memory-utilization 0.85 \
  --max-model-len 8192 --enforce-eager --trust-remote-code \
  --speculative-config '{"method":"dspark","model":"<checkpoint-1 路径>","num_speculative_tokens":7}'

然后`vllm bench serve`（backend=openai-chat，UltraChat prompts，num_prompts=10，concurrency=10）。

### 8.2 结果（两个 checkpoint 对比）

| 指标 | run1 / ckpt0 (062517) | run2 / ckpt1 (063317) |
| --- | --- | ---|
| specdecodeacceptance_length | 1.707 | 1.891↑ |
| specdecodeacceptance_rate | 10.10 | 12.72 |
| per-pos 接受率 (pos0..6) | 0.423, 0.171, 0.069, 0.027, 0.012, 0.0036, 0.0009 | 0.493, 0.227, 0.098, 0.043, 0.018, 0.0077, 0.0033 |
| output_throughput (tok/s) | 93.4 | 95.6 |
| mean_tpot (ms) | 100.8 | 94.2 |
| num drafts / accepted | 11995 / 8480 | 10832 / 9647 |

### 8.3 结论（确认正常）

1.

accept_length 1.71 → 1.89 单调上升：跨 checkpoint 提升，训练有效，方向正确。

2.

per-position 是正确的几何衰减（0.49→0.23→0.098→0.043→…）：这才是 autoregressive 接受率该有的形状（误差累积导致远位接受率快速下降）。和训练侧的"平"不矛盾——两者度量不同。

3.

推理 pos0 ≈ 0.49：比之前记录的 pos0 推理崩溃（~0.3）明显改善，pos0 推理瓶颈基本解决。

判据：per-position 几何衰减 + acceptlength 随训练上升 = 对齐正确、模型作为 spec decoder 工作正常。目标继续把 acceptlength 推高（>2 更理想）。

## 9. 常见问题排查

### Q1：训练报torch.cuda相关错误（manualseed / emptycache / synchronize）

A：未做 NPU 设备适配。NPU 上必须用`torch.npu.*`。

### Q2：训练指标正常（acc 看着行），但 vLLM bench accept_length 极低（~1.0-1.1）

A：最可能是训练↔推理对齐没对上。按优先级排查：

1.

prev_token off-by-one：确认做了 §5.2(c) 的修改且从头重训。

2.

RoPE`+1`/`include_anchor=True`：确认没做dspark2 的修改（这两个会把训练搞错位）。

3.

checkpoint 对吗：bench 的`speculative_config.model`要指向正确的 checkpoint 目录。

### Q3：per-position 接受率不衰减（全平或全很低）

A：全很低（~0.07）通常是 draft 没训好或 markov 头带偏（参考旧 summary 里`block7-no-Markov`AL=1.09）；全平可能只是 teacher-forced 训练指标的正常表现——以 vLLM bench 的 per-position 为准。

### Q4：accept_len（训练侧）和accept_length（bench）对不上

A：正常。前者是 teacher-forced 口径、训练侧算的；后者是真实 autoregressive 接受长度。bench 的`spec_decode_acceptance_length`才是金标准。

### Q5：train/confidence_loss在涨

A：confidence head 的 target 是 moving target（accept_rate 随 draft 变好而上升），单线性层追不上。只要 main loss（ce/tv）在降就没事。

### Q6：vLLM 加载报架构错（Qwen3DSparkModel找不到 / draft 加载失败）

A：确认 vLLM-Ascend 版本支持`method=dspark`。vllm ascend v0.23.0及以下版本，尚未支持Dspark投机推理，可以通过下面的命令重新安装，可以支持DSpark投机推理。

mkdir -p /home/vllm-spec/vllm-workspace
cd /home/vllm-spec/vllm-workspace

pip install setuptools-rust
git clone https://github.com/vllm-project/vllm.git
cd vllm
git checkout 1f486d9
VLLM_TARGET_DEVICE=empty pip install -e . --no-build-isolation
cd ..

git clone https://github.com/vllm-project/vllm-ascend.git
cd vllm-ascend
git fetch origin pull/11765/head:pr_11765
git checkout pr_11765
pip install -e . --no-build-isolation --trusted-host triton-ascend.osinfra.cn --extra-index-url https://triton-ascend.osinfra.cn/pypi/simple

vllm serve /mnt/cephfs/models/Qwen3-30B-A3B/Qwen/Qwen3-30B-A3B/ \
--tensor-parallel-size 2 \
--port 8646 \
--max-model-len 40960 \
--trust-remote-code \
--reasoning-parser qwen3 \
--compilation-config '{"cudagraph_capture_sizes":[7, 8],"cudagraph_mode":"PIECEWISE"}' \
--speculative-config '{"method":"dspark","model":"/data/path/speculators_new/speculators/output/dspark_qwen3_30b_ultrachat_7k_final_test/checkpoints/latest","num_speculative_tokens":7}'

### Q7：训练日志 warn"All N draft layers using sliding window attention..."

A：说明没配`--full-attention-indices`，所有 draft 层都开了 sliding window。这会让训练 attention（block 内 causal + context 截断）和推理（永远 full non-causal）双重不一致，必须配`--full-attention-indices 0 .. num_layers-1`（覆盖全部层）。详见 §6.3.1。改完从头重训。

边框设置

无框线

边距

宽度

1磅

颜色

自由布局设置

整体布局

子模块

评论

修订记录

对正文进行的文本增删、样式修改都将标记为修订

自定义多级列表

列表设置

- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9

- 1.
- a.
- i.
- 1.
- a.
- i.
- 1.
- a.
- i.

前缀

无

序号

1 2 3 ...

后缀

.

编号格式

列表显示

继承层级

不继承

位置

对齐方式

默认

单元格边距

边距

默认

左边距

cm

右边距

cm

上边距

cm

下边距

cm

点赞 0

本页内容

1. DSpark 投机推理介绍 [了解详情](https://www.hiascend.com/#3rsSLf2BvpO1aLpvdzlmzR)

2. 训练环境 [了解详情](https://www.hiascend.com/#3Cn3IEVleAyTVUqntT3ylc)

3. speculators 工具介绍 [了解详情](https://www.hiascend.com/#5fQNjmas3TPapukWUu2Un0)

4. 安装过程 [了解详情](https://www.hiascend.com/#7GpXEVK9kSShMhwylKUvN8)

5. 代码修改说明 [了解详情](https://www.hiascend.com/#67LPZ5v78EFIkqCo1I4Ofq)

6. 训练详细操作 [了解详情](https://www.hiascend.com/#2aW2Fgkro2Q0klYk5EXlA3)

7. 训练结果分析 [了解详情](https://www.hiascend.com/#2pdzhUDyz1uToFhvi6vOTI)

8. vLLM bench 结果 [了解详情](https://www.hiascend.com/#309lDftpCKiqwiHcmpn97N)

9. 常见问题排查 [了解详情](https://www.hiascend.com/#4OMjCREzXuIxyJ7FirKDM0)
