Skip to content

Commit 2bca273

Browse files
committed
fix offline batch chunked prefill
Apply the effective prefill token budget to offline batch generation instead of dispatching the entire prompt batch at once. Preserve normal batched prefill when the full input fits the budget, and process over-budget long prefills one request at a time in round-robin order while retaining absolute positions, cumulative sequence lengths, and KV allocations. Keep Qwen warmup within the same safety threshold, avoid device sampling on intermediate chunks, and cover host- and device-embedding chunk paths with batching tests. The issue #91 workload completes prefill and 20-token decode without the AICPU 507018 failure.
1 parent a77f42c commit 2bca273

4 files changed

Lines changed: 328 additions & 33 deletions

File tree

pypto_serving/config/types.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ class RuntimeConfig:
6868
npu_memory_utilization: float = 0.90
6969
# Max tokens processed per scheduling step (chunked-prefill granularity).
7070
max_num_batched_tokens: int = 4096
71+
# Per-dispatch safety cap for long offline prefill; zero disables the cap.
72+
long_prefill_token_threshold: int = 256
7173
# Compile-time generation limit used by model-specific runners.
7274
max_new_tokens: int = 256
7375

pypto_serving/model/qwen/npu_runner.py

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -402,15 +402,16 @@ def _warmup_dispatch(self, runtime: RuntimeConfig) -> None:
402402
fused prefill) fails at startup rather than on the first real
403403
request.
404404
"""
405-
batch = runtime.max_batch_size
405+
kernel_batch = runtime.max_batch_size
406406
max_seq = runtime.max_seq_len
407-
mnb = getattr(runtime, "max_num_batched_tokens", 4096)
408-
step_tokens = min(mnb, batch * max_seq)
409-
per_req = max(step_tokens // batch, 1)
410-
total_tokens = per_req * batch
407+
batch, per_req, total_tokens = self._warmup_prefill_shape(runtime)
408+
mnb = runtime.max_num_batched_tokens
409+
long_prefill_threshold = runtime.long_prefill_token_threshold
411410

412411
logger.info(
413-
f"[warmup] starting (batch={batch}, max_num_batched_tokens={mnb}, "
412+
f"[warmup] starting (prefill_batch={batch}, kernel_batch={kernel_batch}, "
413+
f"max_num_batched_tokens={mnb}, "
414+
f"long_prefill_token_threshold={long_prefill_threshold}, "
414415
f"max_seq={max_seq}, per_req={per_req}, total_tokens={total_tokens}, slot=-1)",
415416

416417
)
@@ -459,19 +460,19 @@ def _warmup_dispatch(self, runtime: RuntimeConfig) -> None:
459460
compiled.decode_block_table_buffer.fill_(0) # all reads from page 0
460461
compiled.decode_slot_mapping_buffer.fill_(-1) # all writes to page 0
461462

462-
for b in range(batch):
463+
for b in range(kernel_batch):
463464
compiled.decode_seq_lens_buffer[b] = min(per_req + 1, max_seq)
464465

465466
decode_kernel_inputs = _DecodeKernelInputs(
466-
actual_batch=batch,
467+
actual_batch=kernel_batch,
467468
token_ids=compiled.decode_token_ids_buffer,
468469
seq_lens=compiled.decode_seq_lens_buffer,
469470
block_table=compiled.decode_block_table_buffer,
470471
slot_mapping=compiled.decode_slot_mapping_buffer,
471472
logits=compiled.decode_logits_buffer,
472473
)
473474

474-
logger.info(f"[warmup] decode dispatch … (batch={batch}, seq_len={per_req + 1})")
475+
logger.info(f"[warmup] decode dispatch … (batch={kernel_batch}, seq_len={per_req + 1})")
475476
t0 = time.perf_counter()
476477
self._run_distributed_program(
477478
compiled.decode,
@@ -481,6 +482,21 @@ def _warmup_dispatch(self, runtime: RuntimeConfig) -> None:
481482

482483
logger.info("[warmup] complete")
483484

485+
@staticmethod
486+
def _warmup_prefill_shape(runtime: RuntimeConfig) -> tuple[int, int, int]:
487+
"""Return active batch, tokens per request, and total warmup tokens."""
488+
max_batch = runtime.max_batch_size
489+
effective_budget = runtime.max_num_batched_tokens
490+
if runtime.long_prefill_token_threshold > 0:
491+
effective_budget = min(effective_budget, runtime.long_prefill_token_threshold)
492+
step_tokens = min(effective_budget, max_batch * runtime.max_seq_len)
493+
if step_tokens <= 0:
494+
raise ValueError("warmup prefill token budget must be positive")
495+
active_batch = min(max_batch, step_tokens)
496+
per_request_tokens = step_tokens // active_batch
497+
total_tokens = per_request_tokens * active_batch
498+
return active_batch, per_request_tokens, total_tokens
499+
484500
def _alloc_kv_cache_tensor(self, shape: tuple[int, ...], dtype: torch.dtype) -> DeviceTensor:
485501
"""Allocate one worker-resident KV cache tensor shared by prefill/decode."""
486502
return self._shared_l3_worker().alloc_tensor(shape, dtype)

pypto_serving/serving/engine/engine.py

Lines changed: 160 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -207,26 +207,39 @@ def _generate_batch_impl(
207207
allow_device_greedy_sampling=allow_device_greedy_sampling,
208208
kv_allocations=allocations,
209209
)
210-
fast_path_result = self._executor.try_generate_batch(
211-
record,
212-
requests,
213-
prefill_batch,
214-
generate_config,
215-
)
216-
if fast_path_result is not None:
217-
return fast_path_result
218-
219-
with self._executor.session():
220-
prefill_result = self._executor.run_prefill(
221-
runtime_model,
210+
prefill_token_budget = record.runtime.max_num_batched_tokens
211+
if prefill_token_budget <= 0:
212+
raise ValueError("max_num_batched_tokens must be positive")
213+
long_prefill_threshold = record.runtime.long_prefill_token_threshold
214+
if long_prefill_threshold > 0:
215+
prefill_token_budget = min(prefill_token_budget, long_prefill_threshold)
216+
total_prefill_tokens = sum(len(token_ids) for token_ids in prompt_token_ids)
217+
batch_fits_budget = total_prefill_tokens <= prefill_token_budget
218+
if batch_fits_budget:
219+
fast_path_result = self._executor.try_generate_batch(
220+
record,
221+
requests,
222222
prefill_batch,
223+
generate_config,
223224
)
224-
prefill_logits = prefill_result.logits
225-
prefill_sampled_token_ids = (
226-
prefill_result.sampled_token_ids
227-
if allow_device_greedy_sampling
228-
else None
229-
)
225+
if fast_path_result is not None:
226+
return fast_path_result
227+
228+
with self._executor.session():
229+
if batch_fits_budget:
230+
prefill_result = self._executor.run_prefill(runtime_model, prefill_batch)
231+
prefill_logits = prefill_result.logits
232+
prefill_sampled_token_ids = (
233+
prefill_result.sampled_token_ids
234+
if prefill_batch.allow_device_greedy_sampling
235+
else None
236+
)
237+
else:
238+
prefill_logits, prefill_sampled_token_ids = self._run_prefill_in_chunks(
239+
runtime_model,
240+
prefill_batch,
241+
prefill_token_budget,
242+
)
230243

231244
sampling_params = self._sampler.from_generate_config(generate_config)
232245
current_tokens = self._sample_batch_rows(
@@ -420,6 +433,135 @@ def _generate_result(self, model_id: str, prompt: str, config: GenerateConfig) -
420433
"""Generate one result by reusing the batch path."""
421434
return self.generate_batch(model_id, [prompt], config)[0]
422435

436+
def _run_prefill_in_chunks(
437+
self,
438+
runtime_model,
439+
batch: PrefillBatch,
440+
token_budget: int,
441+
) -> tuple[torch.Tensor, torch.Tensor | None]:
442+
"""Run prefill calls whose combined chunk size does not exceed the budget."""
443+
if token_budget <= 0:
444+
raise ValueError("max_num_batched_tokens must be positive")
445+
446+
row_count = len(batch.request_ids)
447+
prompt_lengths = [int(batch.seq_lens[row].item()) for row in range(row_count)]
448+
computed_tokens = [0] * row_count
449+
final_logits: list[torch.Tensor | None] = [None] * row_count
450+
final_sampled_ids: list[torch.Tensor | None] = [None] * row_count
451+
next_request_idx = 0
452+
453+
while any(computed_tokens[row] < prompt_lengths[row] for row in range(row_count)):
454+
active_rows: list[int] = []
455+
for offset in range(row_count):
456+
row = (next_request_idx + offset) % row_count
457+
if computed_tokens[row] < prompt_lengths[row]:
458+
active_rows.append(row)
459+
selected_rows = active_rows[:1]
460+
per_request_budget = token_budget
461+
chunk_lengths = [
462+
min(prompt_lengths[row] - computed_tokens[row], per_request_budget)
463+
for row in selected_rows
464+
]
465+
max_chunk_len = max(chunk_lengths)
466+
467+
chunk_token_ids = batch.token_ids.new_zeros((len(selected_rows), max_chunk_len))
468+
chunk_embeddings = None
469+
if batch.input_embeddings is not None:
470+
chunk_embeddings = batch.input_embeddings.new_zeros(
471+
(len(selected_rows), max_chunk_len, *batch.input_embeddings.shape[2:])
472+
)
473+
chunk_positions = torch.full(
474+
(len(selected_rows), max_chunk_len),
475+
-1,
476+
dtype=torch.long,
477+
device=batch.token_ids.device,
478+
)
479+
chunk_seq_lens = batch.seq_lens.new_empty((len(selected_rows),))
480+
481+
for chunk_row, (request_row, chunk_len) in enumerate(
482+
zip(selected_rows, chunk_lengths, strict=True)
483+
):
484+
chunk_start = computed_tokens[request_row]
485+
chunk_end = chunk_start + chunk_len
486+
chunk_token_ids[chunk_row, :chunk_len] = batch.token_ids[
487+
request_row, chunk_start:chunk_end
488+
]
489+
if chunk_embeddings is not None and batch.input_embeddings is not None:
490+
chunk_embeddings[chunk_row, :chunk_len] = batch.input_embeddings[
491+
request_row, chunk_start:chunk_end
492+
]
493+
chunk_positions[chunk_row, :chunk_len] = torch.arange(
494+
chunk_start,
495+
chunk_end,
496+
dtype=torch.long,
497+
device=batch.token_ids.device,
498+
)
499+
chunk_seq_lens[chunk_row] = chunk_end
500+
501+
prefill_result = self._executor.run_prefill(
502+
runtime_model,
503+
PrefillBatch(
504+
request_ids=[batch.request_ids[row] for row in selected_rows],
505+
token_ids=chunk_token_ids,
506+
input_embeddings=chunk_embeddings,
507+
seq_lens=chunk_seq_lens,
508+
allow_device_greedy_sampling=(
509+
batch.allow_device_greedy_sampling
510+
and any(
511+
computed_tokens[row] + chunk_len == prompt_lengths[row]
512+
for row, chunk_len in zip(
513+
selected_rows,
514+
chunk_lengths,
515+
strict=True,
516+
)
517+
)
518+
),
519+
kv_allocations=[batch.kv_allocations[row] for row in selected_rows],
520+
positions=chunk_positions,
521+
block_ids=(
522+
[batch.block_ids[row] for row in selected_rows]
523+
if batch.block_ids
524+
else []
525+
),
526+
),
527+
)
528+
529+
for chunk_row, (request_row, chunk_len) in enumerate(
530+
zip(selected_rows, chunk_lengths, strict=True)
531+
):
532+
computed_tokens[request_row] += chunk_len
533+
if computed_tokens[request_row] != prompt_lengths[request_row]:
534+
continue
535+
final_logits[request_row] = self._select_batch_row(
536+
prefill_result.logits,
537+
chunk_row,
538+
).clone()
539+
sampled_ids = (
540+
prefill_result.sampled_token_ids
541+
if batch.allow_device_greedy_sampling
542+
else None
543+
)
544+
if sampled_ids is not None:
545+
if sampled_ids.dim() == 0:
546+
sampled_id = sampled_ids
547+
elif sampled_ids.dim() == 1:
548+
sampled_id = sampled_ids[chunk_row]
549+
else:
550+
sampled_id = sampled_ids[chunk_row].reshape(-1)[0]
551+
final_sampled_ids[request_row] = sampled_id.clone()
552+
553+
next_request_idx = (selected_rows[-1] + 1) % row_count
554+
555+
if any(logits is None for logits in final_logits):
556+
raise RuntimeError("prefill did not produce final logits for every request")
557+
logits = torch.stack([row for row in final_logits if row is not None])
558+
sampled_ids = None
559+
if all(sampled_id is not None for sampled_id in final_sampled_ids):
560+
sampled_ids = torch.stack(
561+
[sampled_id for sampled_id in final_sampled_ids if sampled_id is not None]
562+
)
563+
return logits, sampled_ids
564+
423565
def _sample_batch_rows(
424566
self,
425567
logits: torch.Tensor | None,

0 commit comments

Comments
 (0)