Skip to content

Commit 0c8c70c

Browse files
author
fengyan.119
committed
feat: add NPU DFlash block-diffusion speculative decoding.
Introduce DFlash (block-diffusion) speculative decoding on the NPU/ATB backend. The draft proposes a full block of candidate tokens in a single non-causal forward from the target's captured intermediate-layer hidden states, and the target verifies the block via the shared rejection sampler. Supported targets: - Qwen3 dense (e.g. Qwen3-8B) and Qwen3-MoE (e.g. Qwen3-Coder-30B-A3B) as the verify target. Both are standard-attention, head_dim=128, 4D dense KV cache; aux-hidden capture is enabled on qwen3.h and qwen3_moe.h. - The draft is always a small dense Qwen3 (model_type=DFlashDraftModel), regardless of whether the target is dense or MoE; it never runs experts. - Hybrid / linear-attention targets (e.g. Qwen3.5/Next) are NOT supported in this PR (deferred: they need a TORCH-backend draft and target-side capture). Core pieces: - DFlashWorkerImpl (runtime): prepare draft query / run draft / validate / materialize context K/V / write accepted context, decoupled from MTP. - DFlashContextKV helper: backend-independent fc -> RMSNorm -> fused per-layer K/V linear -> per-layer k_norm + RoPE -> scatter, parameterized by DFlashContextKVConfig (head_dim, rotary_dim from partial_rotary_factor, rope_theta, k_norm) so it can be reused across draft bodies. - DFlashQwen3 draft model + registration. - ACL graph integration: DFlash chunked-prefill graph keys, non-causal draft eager fallback, and persistent attn-mask refresh. - Guard: DFlash and EAGLE-3 reject context parallelism (cp_size > 1), which would feed the draft the wrong (lm_head-gathered) hidden states. Correctness fixes folded in: - chunked-prefill graph mask reset stale tail columns [kv_len, max_seq_len) across replays; a heterogeneous-kv_len batch otherwise let a shorter sequence attend to another sequence's KV (graph accept rate 1.11% -> 34.10%, matching eager). - qwen3-moe forward asserts capture_idx == layers_to_capture_set_.size() before returning aux hidden, mirroring qwen3.h, so a misconfigured target_layer_id fails fast instead of feeding uninitialized memory. - dflash validate reconstructs dense draft probs by default (honoring enable_opt_validate_probs like MTP) so the shared rejection sampler and its fused kernel always receive dim==3 probs; output-distribution-equivalent to selected-only. scatter_selected_to_dense() is shared with build_validate_tensors. Validated (gsm8k, greedy, num_spec=15, graph vs eager): - Qwen3-8B: accept rate 34.67% / accept length 6.20, graph == eager bit-identical; matches the transformers reference (34.87% / 6.23). - Qwen3-Coder-30B-A3B (MoE, pure TP=2): accept rate 25.0% / 4.75 end-to-end.
1 parent 044aede commit 0c8c70c

25 files changed

Lines changed: 2280 additions & 113 deletions

xllm/core/distributed_runtime/llm_engine.cpp

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -182,12 +182,17 @@ bool LLMEngine::init_model(MasterStatus master_status) {
182182
auto model_loader = ModelLoader::create(model_path);
183183
LOG(INFO) << "Initializing model from: " << model_path;
184184

185-
tokenizer_ = model_loader->tokenizer();
186-
CHECK(tokenizer_ != nullptr);
187-
188185
args_ = model_loader->model_args();
189186
quant_args_ = model_loader->quant_args();
190-
tokenizer_args_ = model_loader->tokenizer_args();
187+
188+
// Every speculative algorithm drives its draft from the target's hidden
189+
// states and shares the target vocabulary, so a draft engine never needs its
190+
// own tokenizer.
191+
if (!options_.is_draft_engine()) {
192+
tokenizer_ = model_loader->tokenizer();
193+
CHECK(tokenizer_ != nullptr);
194+
tokenizer_args_ = model_loader->tokenizer_args();
195+
}
191196

192197
// compute the number of local kv heads and head dim
193198
const uint32_t world_size = dp_local_tp_size_;
@@ -213,21 +218,23 @@ bool LLMEngine::init_model(MasterStatus master_status) {
213218
<< ", dtype: " << dtype_
214219
<< ", kv_cache_dtype: " << options_.kv_cache_dtype();
215220

216-
const int64_t tokenizer_vocab_size = tokenizer_->vocab_size();
217-
int64_t model_vocab_size = args_.vocab_size();
218-
if (tokenizer_vocab_size != model_vocab_size) {
219-
// use tokenizer vocab size if model vocab size is not set
220-
if (model_vocab_size <= 0) {
221-
LOG(WARNING) << "Model vocab size is not set, using tokenizer vocab "
222-
"size: "
223-
<< tokenizer_vocab_size;
224-
args_.vocab_size(tokenizer_vocab_size);
225-
} else if (tokenizer_vocab_size > model_vocab_size) {
226-
LOG(WARNING) << "Unsafe vocab mismatch: tokenizer: "
227-
<< tokenizer_vocab_size << ", model: " << model_vocab_size;
228-
} else {
229-
LOG(INFO) << "Tokenizer/model vocab differ: tokenizer="
230-
<< tokenizer_vocab_size << ", model=" << model_vocab_size;
221+
if (tokenizer_ != nullptr) {
222+
const int64_t tokenizer_vocab_size = tokenizer_->vocab_size();
223+
int64_t model_vocab_size = args_.vocab_size();
224+
if (tokenizer_vocab_size != model_vocab_size) {
225+
// use tokenizer vocab size if model vocab size is not set
226+
if (model_vocab_size <= 0) {
227+
LOG(WARNING) << "Model vocab size is not set, using tokenizer vocab "
228+
"size: "
229+
<< tokenizer_vocab_size;
230+
args_.vocab_size(tokenizer_vocab_size);
231+
} else if (tokenizer_vocab_size > model_vocab_size) {
232+
LOG(WARNING) << "Unsafe vocab mismatch: tokenizer: "
233+
<< tokenizer_vocab_size << ", model: " << model_vocab_size;
234+
} else {
235+
LOG(INFO) << "Tokenizer/model vocab differ: tokenizer="
236+
<< tokenizer_vocab_size << ", model=" << model_vocab_size;
237+
}
231238
}
232239
}
233240

xllm/core/distributed_runtime/speculative_engine.cpp

Lines changed: 4 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ SpeculativeEngine::SpeculativeEngine(const runtime::Options& options,
5555
.devices(options.draft_devices())
5656
.num_decoding_tokens(1)
5757
.enable_speculative_decode(/*enable_speculative_decode=*/false)
58-
.enable_graph(/*enable_graph=*/false)
58+
.enable_graph(options_.is_dflash() ? options_.enable_graph() : false)
5959
.is_draft_engine(true);
6060
draft_engine_ =
6161
std::make_unique<LLMEngine>(draft_engine_options, dist_manager_);
@@ -97,27 +97,9 @@ bool SpeculativeEngine::init_model() {
9797
return false;
9898
}
9999

100-
// check if the tokenizers are compatible
101-
const auto* draft_tokenizer = draft_engine_->tokenizer();
102-
const auto* target_tokenizer = engine_->tokenizer();
103-
if (draft_tokenizer->vocab_size() != target_tokenizer->vocab_size()) {
104-
LOG(ERROR) << "draft and target tokenizers have different vocab sizes, "
105-
"draft vocab_size: "
106-
<< draft_tokenizer->vocab_size()
107-
<< ", target vocab_size: " << target_tokenizer->vocab_size();
108-
return false;
109-
}
110-
111-
const std::string test_text = "hello from xllm!";
112-
std::vector<int32_t> draft_token_ids;
113-
std::vector<int32_t> target_token_ids;
114-
if (!draft_tokenizer->encode(test_text, &draft_token_ids) ||
115-
!target_tokenizer->encode(test_text, &target_token_ids)) {
116-
if (draft_token_ids != target_token_ids) {
117-
LOG(ERROR) << "draft and target tokenizers are not compatible";
118-
return false;
119-
}
120-
}
100+
// Modern speculative algorithms (MTP / EAGLE / EAGLE3 / DFlash) drive the
101+
// draft from the target's hidden states and share the target vocabulary, so
102+
// there is no independent draft tokenizer to validate for compatibility.
121103

122104
// check if the max context length are the same
123105
const auto& draft_model_args = draft_engine_->model_args();

xllm/core/distributed_runtime/worker_server.cpp

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ limitations under the License.
5454
#include "util/net.h"
5555
#include "util/threadpool.h"
5656
#include "util/timer.h"
57+
#include "util/utils.h"
5758
#if defined(USE_NPU)
5859
#include "hccl/hccl.h"
5960
#include "xllm_atb_layers/core/include/atb_speed/base/external_comm_manager.h"
@@ -66,6 +67,13 @@ extern char** environ;
6667
namespace xllm {
6768
namespace {
6869
void handle_signal(int signum) { _exit(0); }
70+
71+
bool uses_aux_hidden_speculative_worker(const runtime::Options& options) {
72+
if (!options.enable_speculative_decode()) {
73+
return false;
74+
}
75+
return util::algorithm_captures_aux_hidden(options.speculative_algorithm());
76+
}
6977
} // namespace
7078

7179
void WorkerServer::create_server(const runtime::Options& options,
@@ -364,6 +372,11 @@ WorkerServer::WorkerServer(int32_t local_worker_idx,
364372
bool use_spawn_worker)
365373
: server_name_("DistributeWorkerServer") {
366374
server_name_.append(std::to_string(options.server_idx()));
375+
if (use_spawn_worker && uses_aux_hidden_speculative_worker(options)) {
376+
LOG(FATAL) << options.speculative_algorithm()
377+
<< " does not support spawned workers yet. Disable offline "
378+
"spawn workers or use --speculative_algorithm=MTP.";
379+
}
367380

368381
if (worker_type == WorkerType::LLM || worker_type == WorkerType::ELM ||
369382
worker_type == WorkerType::VLM || worker_type == WorkerType::EVLM ||

xllm/core/framework/config/speculative_config.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ DEFINE_int32(num_speculative_tokens, 0, "Number of speculative tokens.");
3333
DEFINE_string(speculative_algorithm,
3434
"MTP",
3535
"Speculative decoding algorithm. Supported options: MTP, Eagle3, "
36-
"Suffix. Default is MTP.");
36+
"Suffix, DFlash. Default is MTP.");
3737

3838
DEFINE_int32(speculative_suffix_cache_max_depth,
3939
64,

xllm/core/framework/model/model_input_params.h

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -713,6 +713,10 @@ struct ModelEmbeddingInput {
713713
// input embedding
714714
mutable torch::Tensor input_embedding;
715715

716+
// Input is target hidden fed through the context-K/V materialization path
717+
// (the model reads input_embedding, not token_ids).
718+
bool write_context_kv = false;
719+
716720
// embedding ids of each sequence
717721
std::vector<int32_t> embedding_ids;
718722

@@ -739,6 +743,7 @@ struct ModelEmbeddingInput {
739743
ModelEmbeddingInput to(const torch::Device& device) const {
740744
ModelEmbeddingInput out;
741745
out.input_embedding = safe_to(input_embedding, device);
746+
out.write_context_kv = write_context_kv;
742747
out.embedding_ids = embedding_ids;
743748
out.linear_state_ids = linear_state_ids;
744749
out.linear_state_indices = safe_to(linear_state_indices, device, true);
@@ -883,6 +888,12 @@ struct ExpertInput {
883888
struct GraphInput {
884889
torch::Tensor attn_mask;
885890
torch::Tensor tiling_data;
891+
bool is_block_diffusion_graph = false;
892+
// Whether the attention is causal. Defaults to true (standard autoregressive
893+
// masking). Block-diffusion drafts set this false: the block's candidate
894+
// tokens attend to the full context and not to each other, while the target
895+
// validate path stays causal.
896+
bool causal = true;
886897
bool use_expanded_decode_for_spec_verify_attention = false;
887898
torch::Tensor expanded_kv_seq_lens;
888899
torch::Tensor expanded_block_tables;
@@ -897,6 +908,8 @@ struct GraphInput {
897908
GraphInput out;
898909
out.attn_mask = safe_to(attn_mask, device, true);
899910
out.tiling_data = safe_to(tiling_data, device, true);
911+
out.is_block_diffusion_graph = is_block_diffusion_graph;
912+
out.causal = causal;
900913
out.use_expanded_decode_for_spec_verify_attention =
901914
use_expanded_decode_for_spec_verify_attention;
902915
out.expanded_kv_seq_lens = safe_to(expanded_kv_seq_lens, device, true);

xllm/core/layers/common/attention_metadata_builder.cpp

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,9 +121,13 @@ AttentionMetadata build_attention_metadata(
121121
bool is_spec_verify_chunked_prefill =
122122
params.is_spec_verify &&
123123
params.meta.batch_forward_type.is_chunked_prefill();
124+
bool is_block_diffusion_chunked_prefill =
125+
params.graph.is_block_diffusion_graph &&
126+
params.meta.batch_forward_type.is_chunked_prefill();
124127
bool use_acl_graph = ::xllm::ExecutionConfig::get_instance().enable_graph() &&
125128
params.enable_graph &&
126-
(is_decode || is_spec_verify_chunked_prefill) &&
129+
(is_decode || is_spec_verify_chunked_prefill ||
130+
is_block_diffusion_chunked_prefill) &&
127131
params.graph.tiling_data.defined();
128132
if (use_acl_graph) {
129133
// ACL graph mode: use CustomPagedAttention with tiling_data on device

xllm/core/runtime/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ cc_library(
3636
mtp_worker_impl.h
3737
suffix_worker_impl.h
3838
eagle3_worker_impl.h
39+
dflash_worker_impl.h
3940
spec_input_builder.h
4041
forward_shared_memory_manager.h
4142
worker_rendezvous.h
@@ -68,6 +69,7 @@ cc_library(
6869
mtp_worker_impl.cpp
6970
suffix_worker_impl.cpp
7071
eagle3_worker_impl.cpp
72+
dflash_worker_impl.cpp
7173
spec_input_builder.cpp
7274
forward_shared_memory_manager.cpp
7375
cp_input_partition.cpp

0 commit comments

Comments
 (0)