Skip to content

Commit 96bf098

Browse files
Clarity256cmcamdy
andauthored
[XPU][Speculative Decoding] Enable CudaGraph capture for MTP draft model (#8061)
- Enable step_use_cudagraph for draft model with proper gating logic - Pass forward_meta and use_cudagraph to xpu_pre_process in draft path - Add padding_cudagraph_inputs() for draft model buffer management - Slice model output by real_token_num when graph is active - Adapt target model warmup and execute_model for MTP+CudaGraph - Use build_sampling_params kernel in verify path (replaces padding_sampling_params) - Fix memory issue by using copy_ instead of clone for seq_lens_this_time - Fix expected_decode_len for TP>1 in dummy_prefill Co-authored-by: cmcamdy <1027740945@qq.com>
1 parent 55edabb commit 96bf098

5 files changed

Lines changed: 109 additions & 59 deletions

File tree

fastdeploy/model_executor/layers/sample/sampler.py

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,12 @@
6161
build_sampling_params_logprob,
6262
naive_update_model_status,
6363
)
64+
elif current_platform.is_xpu():
65+
from fastdeploy.model_executor.ops.xpu import (
66+
build_sampling_params,
67+
top_p_candidates,
68+
verify_draft_tokens,
69+
)
6470

6571

6672
def _apply_triton_top_k_top_p(
@@ -1232,19 +1238,12 @@ def _normal_sample_xpu(
12321238
share_inputs: List[paddle.Tensor],
12331239
) -> SamplerOutput:
12341240
"""Normal sampling for NAIVE mode on XPU."""
1235-
top_p, top_k, topp_seed = padding_sampling_params(
1236-
sampling_metadata.top_p,
1237-
sampling_metadata.top_k,
1238-
sampling_metadata.seed,
1239-
paddle.reshape(share_inputs["seq_lens_this_time"], shape=[-1]),
1240-
paddle.reshape(share_inputs["seq_lens_encoder"], shape=[-1]),
1241-
)
12421241
_, next_tokens = top_k_top_p_sampling(
12431242
probs,
1244-
top_p=top_p,
1245-
top_k=top_k,
1243+
top_p=sampling_metadata.top_p,
1244+
top_k=sampling_metadata.top_k,
12461245
top_k_list=sampling_metadata.top_k_list,
1247-
topp_seed=topp_seed,
1246+
topp_seed=sampling_metadata.seed,
12481247
)
12491248
real_bsz = share_inputs["seq_lens_this_time"].shape[0]
12501249
running_mask = (paddle.reshape(share_inputs["seq_lens_this_time"], shape=[-1]) > 0).cast("int32")
@@ -1264,25 +1263,24 @@ def _verify_and_sample_xpu(
12641263
sampling_metadata: SamplingMetadata,
12651264
max_model_len: int,
12661265
share_inputs: List[paddle.Tensor],
1266+
increment_value: int,
12671267
accept_all_drafts: bool = False,
12681268
reject_all_drafts: bool = False,
12691269
) -> SamplerOutput:
12701270
"""Verify draft tokens (MTP/Ngram mode) on XPU using verify_draft_tokens."""
1271-
from fastdeploy.model_executor.ops.xpu import (
1272-
top_p_candidates,
1273-
verify_draft_tokens,
1274-
)
12751271

12761272
target_tokens = None
12771273
candidate_ids, candidate_scores, candidate_lens = None, None, None
12781274

12791275
if self.verify_strategy == VerifyStrategy.TARGET_MATCH:
1280-
top_p, top_k, topp_seed = padding_sampling_params(
1276+
top_p, top_k, topp_seed = build_sampling_params(
12811277
sampling_metadata.top_p,
12821278
sampling_metadata.top_k,
12831279
sampling_metadata.seed,
1284-
paddle.reshape(share_inputs["seq_lens_this_time"], shape=[-1]),
1285-
paddle.reshape(share_inputs["seq_lens_encoder"], shape=[-1]),
1280+
share_inputs["seq_lens_this_time"],
1281+
share_inputs["seq_lens_encoder"],
1282+
token_num_output_cpu=int(share_inputs["cu_seqlens_q_output"][-1]),
1283+
increment_value=increment_value,
12861284
)
12871285
_, target_tokens = top_k_top_p_sampling(
12881286
probs,
@@ -1344,6 +1342,7 @@ def forward_xpu(
13441342
sampling_metadata: SamplingMetadata,
13451343
max_model_len: int,
13461344
share_inputs: List[paddle.Tensor],
1345+
increment_value: int,
13471346
accept_all_drafts: bool = False,
13481347
reject_all_drafts: bool = False,
13491348
) -> SamplerOutput:
@@ -1397,6 +1396,7 @@ def forward_xpu(
13971396
sampling_metadata,
13981397
max_model_len,
13991398
share_inputs,
1399+
increment_value,
14001400
accept_all_drafts,
14011401
reject_all_drafts,
14021402
)

fastdeploy/model_executor/xpu_pre_and_post_process.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,12 @@ def xpu_pre_process(
137137
) = speculate_pre_process(
138138
token_num_cpu, input_ids, seq_lens_this_time, draft_tokens, seq_lens_encoder, seq_lens_decoder
139139
)
140-
share_inputs["cu_seqlens_q_output"] = cu_seqlens_q_output
141-
share_inputs["batch_id_per_token_output"] = batch_id_per_token_output
140+
if use_cudagraph:
141+
share_inputs["cu_seqlens_q_output"].copy_(cu_seqlens_q_output, False)
142+
share_inputs["batch_id_per_token_output"].copy_(batch_id_per_token_output, False)
143+
else:
144+
share_inputs["cu_seqlens_q_output"] = cu_seqlens_q_output
145+
share_inputs["batch_id_per_token_output"] = batch_id_per_token_output
142146
else:
143147
(
144148
ids_remove_padding,

fastdeploy/spec_decode/mtp_xpu.py

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,12 @@ def _initialize_forward_meta(self, step_use_cudagraph: bool = False, is_dummy_ru
106106
for attn_backend in self.attn_backends:
107107
attn_backend.init_attention_metadata(self.forward_meta)
108108

109+
# 1. CUDA Graph capture sizes must be recorded in descending order (large → small).
110+
# 2. In multi-step execution, only the first step should be captured.
111+
self.forward_meta.step_use_cudagraph = (
112+
step_use_cudagraph and self.draft_model_use_cudagraph and not (substep > 0 and is_dummy_run)
113+
)
114+
109115
def _propose(self, step_use_cudagraph: bool = False, is_dummy_run: bool = False, real_bsz: int = 0):
110116
"""
111117
Main process for MTP inference.
@@ -126,6 +132,8 @@ def _propose(self, step_use_cudagraph: bool = False, is_dummy_run: bool = False,
126132
self.model_inputs["draft_tokens"],
127133
self.model_inputs["seq_lens_encoder"],
128134
self.model_inputs["seq_lens_decoder"],
135+
forward_meta=self.forward_meta,
136+
use_cudagraph=self.draft_model_use_cudagraph,
129137
num_speculative_tokens=self.speculative_config.num_speculative_tokens,
130138
)
131139

@@ -146,7 +154,12 @@ def _propose(self, step_use_cudagraph: bool = False, is_dummy_run: bool = False,
146154
)
147155
self.model_inputs["attn_mask_offsets"].copy_(attn_mask_offsets, False)
148156

149-
self._initialize_forward_meta()
157+
self._initialize_forward_meta(
158+
step_use_cudagraph=step_use_cudagraph, is_dummy_run=is_dummy_run, substep=substep
159+
)
160+
# Padding inputs for cuda graph
161+
self.padding_cudagraph_inputs()
162+
150163
# Get sampling metadata
151164
self.sampling_metadata = SamplingMetadata(
152165
temperature=self.model_inputs["temperature"],
@@ -168,13 +181,16 @@ def _propose(self, step_use_cudagraph: bool = False, is_dummy_run: bool = False,
168181
)
169182

170183
if self.num_model_steps > 1:
171-
self.model_inputs.last_seq_lens_this_time = paddle.clone(self.model_inputs["seq_lens_this_time"])
172-
184+
self.model_inputs.last_seq_lens_this_time.copy_(self.model_inputs["seq_lens_this_time"], False)
185+
real_num = self.model_inputs["ids_remove_padding"].shape[0]
186+
target_hidden_states = self.model_inputs["target_hidden_states"][:real_num]
173187
model_output = self.model(
174188
ids_remove_padding=self.model_inputs["ids_remove_padding"],
175-
previous_hidden_states=self.model_inputs["target_hidden_states"],
189+
previous_hidden_states=target_hidden_states,
176190
forward_meta=self.forward_meta,
177191
)
192+
if self.forward_meta.step_use_cudagraph:
193+
model_output = model_output[: self.real_token_num]
178194
hidden_states = xpu_process_output(model_output, self.forward_meta, self.model_inputs)
179195
# 4. Compute logits, Sample
180196
logits = self.model.compute_logits(hidden_states, forward_meta=self.forward_meta)
@@ -298,3 +314,16 @@ def _update_status(self):
298314
self.target_model_inputs["seq_lens_encoder"],
299315
self.target_model_inputs["stop_flags"],
300316
)
317+
318+
def padding_cudagraph_inputs(self) -> None:
319+
"""
320+
Clean buffers used for the CUDA graph when replaying the CUDA graph with the padded batch.
321+
In FastDeploy, almost all input tensors have a buffer. So, just keep the buffer clean when replaying the CUDA graph with the padded batch.
322+
"""
323+
# In init_attention_metadata, the decode buffer has already been cleared
324+
325+
# To adapt to CUDA Graph, keep the forward pass at the maximum batch size.
326+
if self.forward_meta.step_use_cudagraph:
327+
self.forward_meta.seq_lens_this_time = self.model_inputs["seq_lens_this_time"]
328+
self.real_token_num = self.forward_meta.ids_remove_padding.shape[0]
329+
return

fastdeploy/worker/xpu_model_runner.py

Lines changed: 53 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -171,9 +171,12 @@ def __init__(
171171
self.share_inputs.init_share_inputs()
172172
self.max_num_seqs = self.fd_config.scheduler_config.max_num_seqs
173173

174+
self.increment_value = (
175+
4 if not self.speculative_decoding else (self.speculative_config.num_speculative_tokens + 1) * 4
176+
)
174177
self.infer_seed_increment = paddle.full(
175178
shape=[self.scheduler_config.max_num_seqs, 1],
176-
fill_value=4,
179+
fill_value=self.increment_value,
177180
dtype="int64",
178181
).cpu()
179182

@@ -837,22 +840,8 @@ def _prepare_inputs(self, is_dummy_run=False) -> None:
837840
if self.use_cudagraph:
838841
# Update Batch type for cuda graph for only_decode_batch
839842
if_only_decode = self.only_decode()
840-
841-
only_decode_use_cudagraph = self.use_cudagraph and if_only_decode
842-
# Update config about moe for better performance
843-
# TODO(wanglongzhi):Modifying the config at runtime is not appropriate; it needs to be moved to forward_meta. It will be used in MoEMethodBase.apply()
844-
if self.fd_config.parallel_config.use_ep and self.fd_config.scheduler_config.splitwise_role == "mixed":
845-
self.fd_config.model_config.moe_phase.phase = "decode" if if_only_decode else "prefill"
846-
if self.speculative_decoding:
847-
self.proposer.fd_config.parallel_config.moe_phase.phase = "decode" if if_only_decode else "prefill"
848-
849-
# Update Batch type for cuda graph for only_prefill_batch
850-
only_prefill_use_cudagraph = self.use_cudagraph and self.cudagraph_only_prefill and self.only_prefill()
851-
852843
self.forward_meta.step_use_cudagraph = (
853-
only_prefill_use_cudagraph
854-
if self.cudagraph_only_prefill
855-
else only_decode_use_cudagraph and self.forward_meta.ids_remove_padding.shape[0] > 0
844+
self.use_cudagraph and if_only_decode and self.forward_meta.ids_remove_padding.shape[0] > 0
856845
)
857846

858847
# Update bad tokens len
@@ -864,11 +853,10 @@ def _prepare_inputs(self, is_dummy_run=False) -> None:
864853
if self.pd_disaggregation_mode == "per_chunk" or self.pd_disaggregation_mode == "per_query":
865854
self.forward_meta.kv_signal_sender = self.share_inputs["kv_signal_sender"]
866855

867-
if (
868-
self.fd_config.scheduler_config.splitwise_role == "mixed" and envs.FD_XPU_ENABLE_MIXED_EP_MODE
869-
): # Centralized scenario: the phase is initialized as "prefill" by default. During inference runtime, different types of batches can achieve phase switching at this point.
856+
if self.fd_config.parallel_config.use_ep and self.fd_config.scheduler_config.splitwise_role == "mixed":
870857
if_only_decode = self.only_decode()
871858
self.fd_config.model_config.moe_phase.phase = "decode" if if_only_decode else "prefill"
859+
# TODO: sync proposer.fd_config.model_config.moe_phase.phase for MTP draft model in mixed EP mode
872860

873861
# Get sampling metadata
874862
# TODU(lilujia): sync with GPU
@@ -1122,6 +1110,7 @@ def _dummy_run(
11221110
batch_size: paddle.Tensor,
11231111
expected_decode_len: int = 1,
11241112
in_capturing: bool = False,
1113+
accept_all_drafts=False,
11251114
) -> paddle.Tensor:
11261115
"""
11271116
Use dummy inputs to run before formal execution.
@@ -1146,11 +1135,11 @@ def _dummy_run(
11461135
self.proposer.dummy_prefill_inputs(
11471136
num_tokens=num_tokens,
11481137
batch_size=batch_size,
1149-
expected_decode_len=1,
1138+
expected_decode_len=expected_decode_len,
11501139
)
11511140

11521141
while True:
1153-
self.execute_model(is_dummy_run=True, in_capturing=in_capturing)
1142+
self.execute_model(is_dummy_run=True, in_capturing=in_capturing, accept_all_drafts=accept_all_drafts)
11541143

11551144
if int((self.share_inputs["seq_lens_this_time"] > 0).sum()) == 0:
11561145
break
@@ -1199,14 +1188,30 @@ def capture_model(self) -> None:
11991188
capture_sizes = self.cudagraph_capture_sizes.copy()
12001189

12011190
try:
1202-
for batch_size in sorted(capture_sizes, reverse=True):
1203-
self._dummy_run(
1204-
num_tokens=self.scheduler_config.max_num_batched_tokens,
1205-
batch_size=batch_size,
1206-
expected_decode_len=expected_decode_len,
1207-
in_capturing=True,
1208-
)
1209-
logger.info(f"Warm up the model with the batch size:{batch_size}, num tokens:{expected_decode_len}")
1191+
if self.speculative_decoding and self.spec_method in [SpecMethod.MTP, SpecMethod.SUFFIX]:
1192+
for capture_size in sorted(capture_sizes, reverse=True):
1193+
expected_decode_len = (self.speculative_config.num_speculative_tokens + 1) * 2
1194+
self._dummy_run(
1195+
num_tokens=self.fd_config.get_max_chunk_tokens(),
1196+
batch_size=int(capture_size / (self.speculative_config.num_speculative_tokens + 1)),
1197+
in_capturing=True,
1198+
expected_decode_len=expected_decode_len,
1199+
accept_all_drafts=True,
1200+
)
1201+
logger.info(
1202+
f"Warm up the model with the num_tokens:{capture_size}, expected_decode_len:{expected_decode_len}"
1203+
)
1204+
else:
1205+
for batch_size in sorted(capture_sizes, reverse=True):
1206+
self._dummy_run(
1207+
num_tokens=self.scheduler_config.max_num_batched_tokens,
1208+
batch_size=batch_size,
1209+
expected_decode_len=expected_decode_len,
1210+
in_capturing=True,
1211+
)
1212+
logger.info(
1213+
f"Warm up the model with the batch size:{batch_size}, num tokens:{expected_decode_len}"
1214+
)
12101215
except RuntimeError as e:
12111216
if "out of memory" in str(e):
12121217
raise RuntimeError(
@@ -1263,6 +1268,7 @@ def execute_model(
12631268
num_running_requests: int = None,
12641269
is_dummy_run: bool = False,
12651270
in_capturing: bool = False,
1271+
accept_all_drafts: bool = False,
12661272
) -> Optional[ModelRunnerOutput]:
12671273
"""
12681274
The Entrance of model execute.
@@ -1276,14 +1282,18 @@ class at the server level, which is too granular for ModelRunner.
12761282
# 0. set debug level
12771283
# self._set_debug_level(0x1, model_forward_batch, is_dummy_run)
12781284
with kv_signal_sender_context_manager(self.pd_disaggregation_mode) as sender:
1279-
12801285
self.share_inputs["kv_signal_sender"] = sender
12811286
# 1. Prepare inputs of model and decoder.
12821287
self._prepare_inputs(is_dummy_run=is_dummy_run)
1288+
# 2. Padding inputs for cuda graph
1289+
self.padding_cudagraph_inputs()
12831290
if is_dummy_run:
12841291
self.forward_meta.step_use_cudagraph = in_capturing and self.forward_meta.step_use_cudagraph
1285-
# 2. Padding inputs for cuda grph
1286-
self.padding_cudagraph_inputs()
1292+
else:
1293+
self.forward_meta.step_use_cudagraph = (
1294+
self.forward_meta.step_use_cudagraph
1295+
and self.real_token_num <= self.fd_config.graph_opt_config.max_capture_size
1296+
)
12871297

12881298
num_tokens = self.share_inputs["ids_remove_padding"].shape[0]
12891299
if not self.parallel_config.enable_expert_parallel and num_tokens <= 0:
@@ -1300,7 +1310,7 @@ class at the server level, which is too granular for ModelRunner.
13001310
model_inputs["ids_remove_padding"] = self.share_inputs["ids_remove_padding"]
13011311
if self.enable_mm:
13021312
model_inputs["image_features"] = self.share_inputs["image_features"]
1303-
# 3. Execute model
1313+
# 3. Execute
13041314
model_output = self.model(
13051315
model_inputs,
13061316
forward_meta=self.forward_meta,
@@ -1331,6 +1341,8 @@ class at the server level, which is too granular for ModelRunner.
13311341
self.sampling_metadata,
13321342
self.model_config.max_model_len,
13331343
self.share_inputs,
1344+
self.increment_value,
1345+
accept_all_drafts=accept_all_drafts,
13341346
)
13351347
if self.parallel_config.tensor_parallel_size > 1:
13361348
paddle.distributed.broadcast(
@@ -1428,13 +1440,18 @@ class at the server level, which is too granular for ModelRunner.
14281440
# 6. Draft model propose
14291441
if self.speculative_decoding and self.proposer is not None:
14301442
if self.spec_method == SpecMethod.MTP:
1431-
self.proposer.run(full_hidden_states=model_output)
1443+
self.proposer.run(
1444+
full_hidden_states=model_output,
1445+
step_use_cudagraph=self.forward_meta.step_use_cudagraph,
1446+
is_dummy_run=is_dummy_run,
1447+
)
14321448
else:
14331449
self.proposer.run(share_inputs=self.share_inputs)
14341450

14351451
# 7. Updata 'infer_seed' and step_paddle()
1436-
self.share_inputs["infer_seed"].add_(self.infer_seed_increment)
1437-
self.share_inputs["infer_seed"][:] %= self.MAX_INFER_SEED
1452+
if not self.speculative_decoding:
1453+
self.share_inputs["infer_seed"].add_(self.infer_seed_increment)
1454+
self.share_inputs["infer_seed"][:] %= self.MAX_INFER_SEED
14381455

14391456
if self.speculative_decoding:
14401457
speculate_schedule_cache(
File renamed without changes.

0 commit comments

Comments
 (0)