Skip to content

Commit 37d0594

Browse files
authored
bugfix: preserve MTP correctness during asynchronous execution. (#1931)
1 parent 57776cb commit 37d0594

6 files changed

Lines changed: 251 additions & 86 deletions

File tree

tests/core/runtime/acl_graph_executor_test.cpp

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ limitations under the License.
2727
#include "core/framework/block/block.h"
2828
#include "core/framework/block/block_manager_impl.h"
2929
#include "core/framework/config/execution_config.h"
30+
#include "core/framework/config/speculative_config.h"
3031
#include "core/framework/kv_cache/kv_cache.h"
3132
#include "core/framework/model/model_args.h"
3233
#include "core/framework/model/model_output.h"
@@ -37,6 +38,7 @@ limitations under the License.
3738
#include "core/layers/npu/npu_lm_head_impl.h"
3839
#include "core/layers/npu/npu_word_embedding_impl.h"
3940
#include "core/runtime/acl_graph_executor_impl.h"
41+
#include "core/runtime/acl_graph_persistent_param.h"
4042
#include "core/runtime/base_executor_impl.h"
4143
#include "core/runtime/options.h"
4244

@@ -770,6 +772,86 @@ TEST_F(AclGraphExecutorTest, GraphDoubleBufferFlagControlsSlotCount) {
770772
original_enable_graph_double_buffer);
771773
}
772774

775+
TEST(AclGraphPersistentParamTest, SpecVerifyMetadataUsesTokenCapacity) {
776+
SpeculativeConfig& speculative_config = SpeculativeConfig::get_instance();
777+
const bool original_enable_atb_spec_kernel =
778+
speculative_config.enable_atb_spec_kernel();
779+
speculative_config.enable_atb_spec_kernel(false);
780+
781+
ModelArgs args;
782+
args.model_type("deepseek_v4");
783+
args.dtype("float32");
784+
args.hidden_size(8);
785+
args.max_position_embeddings(32);
786+
787+
runtime::Options options;
788+
options.block_size(4);
789+
options.max_seqs_per_batch(10);
790+
options.max_tokens_per_batch(64);
791+
options.num_decoding_tokens(3);
792+
options.enable_speculative_decode(true);
793+
options.is_draft_engine(false);
794+
795+
const torch::Device device("npu:0");
796+
::xllm::npu::GraphPersistentParam persistent_param(
797+
args,
798+
device,
799+
options,
800+
/*need_update_attn_mask=*/false,
801+
/*is_hybrid_linear_attention=*/false);
802+
EXPECT_EQ(persistent_param.q_seq_lens().size(0), 30);
803+
EXPECT_EQ(persistent_param.kv_seq_lens().size(0), 30);
804+
EXPECT_EQ(persistent_param.persistent_block_tables().size(0), 30);
805+
806+
constexpr int64_t kValidateRows = 12;
807+
const torch::TensorOptions int_options =
808+
torch::dtype(torch::kInt).device(device);
809+
const torch::Tensor tokens = torch::arange(kValidateRows, int_options);
810+
const torch::Tensor positions = torch::arange(kValidateRows, int_options);
811+
ModelInputParams params;
812+
params.is_spec_verify = true;
813+
params.meta.batch_forward_type = BatchForwardType::DECODE;
814+
params.meta.num_sequences = kValidateRows;
815+
params.attention.host.q_seq_lens.assign(kValidateRows, 1);
816+
params.attention.host.kv_seq_lens.assign(kValidateRows, 8);
817+
params.attention.device.q_seq_lens =
818+
torch::ones({kValidateRows}, int_options);
819+
params.attention.device.kv_seq_lens =
820+
torch::full({kValidateRows}, 8, int_options);
821+
params.attention.device.new_cache_slots =
822+
torch::zeros({kValidateRows}, int_options);
823+
params.attention.device.block_tables =
824+
torch::zeros({kValidateRows, 2}, int_options);
825+
826+
std::optional<ModelInputParams> capture_params;
827+
EXPECT_NO_THROW(capture_params = persistent_param.update(
828+
tokens,
829+
torch::Tensor(),
830+
torch::Tensor(),
831+
positions,
832+
params,
833+
/*padded_num_tokens=*/kValidateRows,
834+
/*return_capture_params=*/true));
835+
EXPECT_TRUE(capture_params.has_value());
836+
if (capture_params.has_value()) {
837+
EXPECT_EQ(capture_params->attention.device.q_seq_lens.size(0),
838+
kValidateRows);
839+
EXPECT_EQ(capture_params->attention.device.block_tables.size(0),
840+
kValidateRows);
841+
}
842+
843+
::xllm::npu::GraphPersistentParam hybrid_persistent_param(
844+
args,
845+
device,
846+
options,
847+
/*need_update_attn_mask=*/false,
848+
/*is_hybrid_linear_attention=*/true);
849+
EXPECT_EQ(hybrid_persistent_param.q_seq_lens().size(0), 10);
850+
EXPECT_EQ(hybrid_persistent_param.persistent_block_tables().size(0), 10);
851+
852+
speculative_config.enable_atb_spec_kernel(original_enable_atb_spec_kernel);
853+
}
854+
773855
TEST(AclGraphExecutorHybridTest, KvCacheSupportsLinearOnlyLayers) {
774856
auto conv_cache = torch::zeros({4, 32, 3}, torch::dtype(torch::kFloat32));
775857
auto ssm_cache = torch::zeros({4, 8, 64, 64}, torch::dtype(torch::kFloat32));

xllm/core/runtime/acl_graph_persistent_param.cpp

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,11 @@ GraphPersistentParam::GraphPersistentParam(const ModelArgs& args,
162162
// only serves decode / spec-verify batches, so the relevant row upper bound
163163
// comes from decode graph capacity instead.
164164
const int64_t max_graph_tokens = get_decode_graph_token_capacity(options);
165-
const int64_t max_seqs_per_batch = get_decode_graph_capacity(options);
165+
// Hybrid spec verify keeps sequence-scoped metadata, while non-hybrid MTP
166+
// expands every proposal token into its own decode metadata row.
167+
const int64_t metadata_capacity = is_hybrid_linear_attention
168+
? get_decode_graph_capacity(options)
169+
: max_graph_tokens;
166170

167171
const int64_t max_seq_len = args_.max_position_embeddings();
168172

@@ -182,18 +186,17 @@ GraphPersistentParam::GraphPersistentParam(const ModelArgs& args,
182186
persistent_new_cache_slots_default_ = torch::zeros(
183187
{max_tokens_per_batch}, torch::dtype(torch::kInt).device(device));
184188
persistent_linear_state_indices_ = torch::zeros(
185-
{max_seqs_per_batch}, torch::dtype(torch::kInt).device(device));
189+
{metadata_capacity}, torch::dtype(torch::kInt).device(device));
186190
persistent_num_accepted_tokens_ = torch::ones(
187-
{max_seqs_per_batch}, torch::dtype(torch::kInt).device(device));
191+
{metadata_capacity}, torch::dtype(torch::kInt).device(device));
188192

189-
// Sequence length tensors with max_seqs_per_batch
190-
q_seq_lens_ = torch::zeros({max_seqs_per_batch},
193+
q_seq_lens_ = torch::zeros({metadata_capacity},
191194
torch::dtype(torch::kInt).device(device));
192-
kv_seq_lens_ = torch::zeros({max_seqs_per_batch},
195+
kv_seq_lens_ = torch::zeros({metadata_capacity},
193196
torch::dtype(torch::kInt).device(device));
194-
q_seq_lens_default_ = torch::ones({max_seqs_per_batch},
197+
q_seq_lens_default_ = torch::ones({metadata_capacity},
195198
torch::dtype(torch::kInt).device(device));
196-
kv_seq_lens_default_ = torch::ones({max_seqs_per_batch},
199+
kv_seq_lens_default_ = torch::ones({metadata_capacity},
197200
torch::dtype(torch::kInt).device(device));
198201
expanded_kv_seq_lens_ = torch::zeros(
199202
{max_tokens_per_batch}, torch::dtype(torch::kInt).device(device));
@@ -203,10 +206,10 @@ GraphPersistentParam::GraphPersistentParam(const ModelArgs& args,
203206
const int64_t max_block_table_len =
204207
(max_seq_len + block_size - 1) / block_size + 1;
205208
persistent_block_tables_ =
206-
torch::zeros({max_seqs_per_batch, max_block_table_len},
209+
torch::zeros({metadata_capacity, max_block_table_len},
207210
torch::dtype(torch::kInt).device(device));
208211
persistent_block_tables_default_ =
209-
torch::zeros({max_seqs_per_batch, max_block_table_len},
212+
torch::zeros({metadata_capacity, max_block_table_len},
210213
torch::dtype(torch::kInt).device(device));
211214
persistent_expanded_block_tables_ =
212215
torch::zeros({max_tokens_per_batch, max_block_table_len},
@@ -243,8 +246,8 @@ GraphPersistentParam::GraphPersistentParam(const ModelArgs& args,
243246
}
244247

245248
q_cu_seq_lens_default_ = torch::zeros(
246-
{max_seqs_per_batch + 1}, torch::dtype(torch::kInt).device(device));
247-
q_cu_seq_lens_ = torch::zeros({max_seqs_per_batch + 1},
249+
{metadata_capacity + 1}, torch::dtype(torch::kInt).device(device));
250+
q_cu_seq_lens_ = torch::zeros({metadata_capacity + 1},
248251
torch::dtype(torch::kInt).device(device));
249252

250253
// Pre-allocate persistent dp/cp ep padding buffers with maximum capacity.
@@ -782,9 +785,15 @@ std::optional<ModelInputParams> GraphPersistentParam::update(
782785

783786
if (persistent_block_tables_default_.defined() &&
784787
persistent_block_tables_default_.sizes() ==
785-
persistent_block_tables_.sizes()) {
786-
persistent_block_tables_.copy_(persistent_block_tables_default_,
787-
/*non_blocking=*/true);
788+
persistent_block_tables_.sizes() &&
789+
padded_batch_size > 0) {
790+
CHECK_LE(padded_batch_size, persistent_block_tables_.size(0))
791+
<< "padded graph metadata rows exceed persistent block table capacity";
792+
persistent_block_tables_
793+
.slice(/*dim=*/0, /*start=*/0, /*end=*/padded_batch_size)
794+
.copy_(persistent_block_tables_default_.slice(
795+
/*dim=*/0, /*start=*/0, /*end=*/padded_batch_size),
796+
/*non_blocking=*/true);
788797
}
789798
if (actual_seq_len_rows > 0 &&
790799
params.attention.device.block_tables.defined() &&

xllm/core/runtime/forward_params.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -908,6 +908,9 @@ struct ForwardOutput {
908908
// Keep no-sync input tensor handles alive until downstream consumers finish
909909
// using outputs on the same compute stream.
910910
std::shared_ptr<ForwardInput> retained_input;
911+
// Composite workers retain nested no-sync inputs until their final output is
912+
// synchronized or its ready event is consumed.
913+
std::vector<std::shared_ptr<ForwardInput>> retained_input_dependencies;
911914
// Device-side readiness dependency for no-sync outputs. This local runtime
912915
// handle is intentionally not included in proto or shared-memory transport.
913916
StreamEventPtr ready_event;

xllm/core/runtime/llm_worker_impl.cpp

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,8 @@ std::optional<ForwardOutput> LLMWorkerImpl::step_no_sync(
124124

125125
std::optional<ForwardOutput> LLMWorkerImpl::execute_no_sync_on_stream(
126126
const ForwardInput& input,
127-
Stream& compute_stream) {
127+
Stream& compute_stream,
128+
bool record_ready_event) {
128129
const ForwardSyncPolicy sync_policy = ForwardSyncPolicy::NO_SYNC;
129130
c10::StreamGuard stream_guard = compute_stream.set_stream_guard();
130131
if (::xllm::LoadConfig::get_instance().enable_manual_loader()) {
@@ -136,19 +137,19 @@ std::optional<ForwardOutput> LLMWorkerImpl::execute_no_sync_on_stream(
136137
const_cast<atb::Context*>(context_.get_atb_context());
137138
atb_context->SetExecuteStream(current_acl_stream);
138139
wait_input_ready_events(input, compute_stream);
139-
return step_internal(input, sync_policy);
140+
return step_internal(input, sync_policy, record_ready_event);
140141
} else {
141142
SET_ATB_EXECUTE_STREAM((&compute_stream), device_, context_);
142143
wait_input_ready_events(input, compute_stream);
143-
return step_internal(input, sync_policy);
144+
return step_internal(input, sync_policy, record_ready_event);
144145
}
145146
#else
146147
wait_input_ready_events(input, compute_stream);
147-
return step_internal(input, sync_policy);
148+
return step_internal(input, sync_policy, record_ready_event);
148149
#endif
149150
}
150151
wait_input_ready_events(input, compute_stream);
151-
return step_internal(input, sync_policy);
152+
return step_internal(input, sync_policy, record_ready_event);
152153
}
153154

154155
std::optional<ForwardOutput> LLMWorkerImpl::step(const ForwardInput& input) {
@@ -213,7 +214,8 @@ LLMWorkerImpl::update_input_by_last_step_output_for_schedule_overlap(
213214

214215
std::optional<ForwardOutput> LLMWorkerImpl::step_internal(
215216
const ForwardInput& input,
216-
ForwardSyncPolicy sync_policy) {
217+
ForwardSyncPolicy sync_policy,
218+
bool record_ready_event) {
217219
MULTI_MODEL_STEP_LOCK(::xllm::KVCacheConfig::get_instance().enable_xtensor());
218220

219221
Timer timer;
@@ -376,7 +378,7 @@ std::optional<ForwardOutput> LLMWorkerImpl::step_internal(
376378
if (sync_policy == ForwardSyncPolicy::NO_SYNC) {
377379
wait_kv_push();
378380
output.retained_input = std::make_shared<ForwardInput>(input);
379-
if (enable_schedule_overlap()) {
381+
if (enable_schedule_overlap() && record_ready_event) {
380382
output.ready_event = record_current_stream_event(device_);
381383
}
382384
return output;

xllm/core/runtime/llm_worker_impl.h

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,14 +51,16 @@ class LLMWorkerImpl : public WorkerImpl {
5151
std::optional<ForwardOutput> step_no_sync(const ForwardInput& input);
5252
std::optional<ForwardOutput> execute_no_sync_on_stream(
5353
const ForwardInput& input,
54-
Stream& compute_stream);
54+
Stream& compute_stream,
55+
bool record_ready_event = true);
5556

5657
folly::SemiFuture<std::optional<ForwardOutput>> step_async_no_sync(
5758
const ForwardInput& input);
5859

5960
std::optional<ForwardOutput> step_internal(
6061
const ForwardInput& input,
61-
ForwardSyncPolicy sync_policy = ForwardSyncPolicy::LEGACY);
62+
ForwardSyncPolicy sync_policy = ForwardSyncPolicy::LEGACY,
63+
bool record_ready_event = true);
6264

6365
protected:
6466
std::optional<ForwardOutput> step_for_schedule_overlap(

0 commit comments

Comments
 (0)