Files
wehub-resource-sync 59a0a3844c
PR Test AMD / cancel-on-close (push) Has been skipped
PR Test NVIDIA ARM / scan (push) Has been skipped
PR Test NVIDIA / cancel-on-close (push) Has been skipped
PR Test AMD / scan (push) Has been skipped
PR Test NVIDIA ARM / cancel-on-close (push) Has been skipped
PR Test NVIDIA / scan (push) Has been skipped
Release Docker Images / build (cu129-torch-2.11.0) (push) Has been skipped
Release Docker Images / build (cu130-torch-2.11.0) (push) Has been skipped
Release PyPI / publish (push) Has been skipped
Scheduler Python Test / test (push) Successful in 27m19s
Docs / build (push) Successful in 28m8s
Scheduler C++ Test / test (push) Successful in 28m19s
Scheduler C++ Test / test-flat (push) Successful in 28m18s
Docs / deploy (push) Has been cancelled
PR Test AMD / finish (push) Has been cancelled
PR Test NVIDIA / finish (push) Has been cancelled
PR Test NVIDIA ARM / finish (push) Has been cancelled
PR Test NVIDIA ARM / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
PR Test AMD / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
PR Test NVIDIA / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:32:31 +08:00

454 lines
21 KiB
Python

# Copyright (c) 2026 LightSeek Foundation
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from __future__ import annotations
from typing import TYPE_CHECKING
import torch
from tokenspeed.runtime.execution.cache_loc_kernel import (
compute_out_cache_loc,
fused_decode_input_prep,
)
from tokenspeed.runtime.execution.forward_batch_info import compute_position_triton
from tokenspeed.runtime.utils import get_colorful_logger
from tokenspeed.runtime.utils.nvtx import nvtx_range
if TYPE_CHECKING:
from tokenspeed.runtime.execution.runtime_states import RuntimeStates
logger = get_colorful_logger(__name__)
class InputBuffers:
"""
ForwardContext tensor data source, read-only after fill. Holds only
model-forward inputs; per-request sampling scalars (temperature, top_k,
penalties, seed, etc.) live on the sampling backend as pool-indexed
buffers populated on slot flips.
"""
def __init__(
self,
max_bs: int,
max_num_tokens: int,
page_size: int,
dummy_kv_slot: int,
state_write_padding_pool_index: int,
device: str = "cuda",
has_mamba: bool = False,
):
self.device = device
self.page_size = page_size
self.max_num_tokens = max_num_tokens
self.dummy_kv_slot = dummy_kv_slot
self.state_write_padding_pool_index = state_write_padding_pool_index
self.max_bs = max_bs
self.all_extends_mid_chunk = False
self.has_mamba = has_mamba
with torch.device(device):
# Initialise buffers to the *padding* values the captured graph
# expects for padded rows (input_ids=1, positions=0, req_pool=0,
# seq_lens=1, out_cache_loc=dummy_kv_slot). Each iteration overwrites
# the active prefix [:total_tokens]; fill_input_buffers refreshes the
# padding tail [total_tokens:] back to these defaults every step,
# because a larger prior iter can leave stale values past the
# current prefix.
self.input_ids_buf = torch.ones((max_num_tokens,), dtype=torch.int32)
# Used in draft prefill
self.shifted_prefill_ids_buf = torch.ones_like(self.input_ids_buf)
self.input_lengths_buf = torch.ones((max_num_tokens,), dtype=torch.int32)
# Zero (not arange) so padded positions read a consistent, in-range
# value; the tail is re-zeroed every iteration by fill_input_buffers.
self.positions_buf = torch.zeros(max_num_tokens, dtype=torch.int64)
self.mrope_positions_buf = torch.zeros(
(3, max_num_tokens), dtype=torch.int64
)
self.req_pool_indices_buf = torch.zeros((max_bs,), dtype=torch.int64)
self.state_write_req_pool_indices_buf = torch.full(
(max_bs,), state_write_padding_pool_index, dtype=torch.int64
)
self.seq_lens_buf = torch.ones((max_bs,), dtype=torch.int32)
# Initialise to dummy_kv_slot so that padding positions (never
# written by compute_out_cache_loc) always point to the reserved
# dummy KV slot and never corrupt real KV cache entries.
self.out_cache_loc_buf = torch.full(
(max_num_tokens,), dummy_kv_slot, dtype=torch.int32
)
self.force_single_token_verify_buf = torch.zeros(max_bs, dtype=torch.bool)
self.extend_prefix_lens_buf = torch.zeros(max_bs, dtype=torch.int32)
self.extend_seq_lens_buf = torch.zeros(max_bs, dtype=torch.int32)
if has_mamba:
self.mamba_pool_indices_buf = torch.full(
(max_bs,), -1, dtype=torch.int32
)
self.mamba_cow_src_indices_buf = torch.full(
(max_bs,), -1, dtype=torch.int32
)
self.mamba_branching_seqlens_buf = torch.full(
(max_bs,), -1, dtype=torch.int32
)
self.mamba_track_pool_indices_buf = torch.full(
(max_bs,), -1, dtype=torch.int32
)
self.extend_prefix_lens_cpu = torch.zeros(
max_bs, dtype=torch.int32, pin_memory=True
)
self.extend_seq_lens_cpu = torch.zeros(
max_bs, dtype=torch.int32, pin_memory=True
)
if has_mamba:
self._mamba_pool_indices_cpu = torch.full(
(max_bs,), -1, dtype=torch.int32, pin_memory=True
)
self._mamba_cow_src_indices_cpu = torch.full(
(max_bs,), -1, dtype=torch.int32, pin_memory=True
)
self._mamba_branching_seqlens_cpu = torch.full(
(max_bs,), -1, dtype=torch.int32, pin_memory=True
)
self._mamba_track_pool_indices_cpu = torch.full(
(max_bs,), -1, dtype=torch.int32, pin_memory=True
)
@nvtx_range("input_prep_fill", color="cyan")
def fill_input_buffers(
self,
forward_op,
runtime_states: RuntimeStates,
req_to_page: torch.Tensor,
total_tokens: int,
):
batch_size = len(forward_op.request_ids)
num_extends = forward_op.num_extends()
# CPU-side fast path: when the scheduler always emits a decode_input_ids
# list (even though every entry is -1, meaning "no override").
decode_input_ids = forward_op.decode_input_ids
if decode_input_ids is not None and all(x == -1 for x in decode_input_ids):
decode_input_ids = None
req_pool_indices_cpu = torch.tensor(
forward_op.request_pool_indices, device="cpu", pin_memory=True
)
self.req_pool_indices_buf[:batch_size].copy_(
req_pool_indices_cpu,
non_blocking=True,
)
self.state_write_req_pool_indices_buf[:batch_size].copy_(
req_pool_indices_cpu,
non_blocking=True,
)
input_lengths_cpu = torch.tensor(
forward_op.input_lengths,
dtype=torch.int32,
device="cpu",
pin_memory=True,
)
self.input_lengths_buf[:batch_size].copy_(
input_lengths_cpu,
non_blocking=True,
)
self.all_extends_mid_chunk = (
num_extends > 0
and num_extends == batch_size
and all(
forward_op.extend_prefix_lens[i] + forward_op.input_lengths[i]
< forward_op.prefill_lengths[i]
for i in range(num_extends)
)
)
if num_extends > 0:
self.extend_prefix_lens_cpu[:num_extends] = torch.as_tensor(
forward_op.extend_prefix_lens, dtype=torch.int32
)
self.extend_prefix_lens_buf[:num_extends].copy_(
self.extend_prefix_lens_cpu[:num_extends], non_blocking=True
)
self.extend_seq_lens_cpu[:num_extends] = torch.as_tensor(
forward_op.input_lengths[:num_extends], dtype=torch.int32
)
self.extend_seq_lens_buf[:num_extends].copy_(
self.extend_seq_lens_cpu[:num_extends], non_blocking=True
)
# Get valid cache lengths for requests
req_pool_indices_device = self.req_pool_indices_buf[:batch_size]
input_lengths_device = self.input_lengths_buf[:batch_size]
def write_decode_input_ids(
decode_req_pool_indices: torch.Tensor,
decode_input_ids: list[int],
row_offset: int,
expected_count: int,
context: str,
) -> None:
if len(decode_input_ids) != expected_count:
raise RuntimeError(
f"{context} decode_input_ids length mismatch: "
f"got {len(decode_input_ids)}, expected {expected_count}"
)
decode_input_ids_tensor = torch.tensor(
decode_input_ids,
dtype=torch.int32,
device="cpu",
pin_memory=True,
).to(req_pool_indices_device.device, non_blocking=True)
mask = (decode_input_ids_tensor != -1).unsqueeze(1)
ids = decode_input_ids_tensor.unsqueeze(1)
# Col 0: verified token (mask preserves drafter-owned rows).
first_slot = runtime_states.future_input_map[decode_req_pool_indices, :1]
runtime_states.future_input_map[decode_req_pool_indices, :1] = torch.where(
mask, ids, first_slot
)
# Cols 1.. are real candidates only when the local drafter or the
# remote P-side path populated them. Bootstrap/recovery rows with
# no candidate source still feed a full-width target forward, so
# use a valid dummy token in model inputs and force the verifier to
# consume only the first target token for those rows.
width = runtime_states.future_input_map.shape[1]
remote_candidate_ready = runtime_states.remote_spec_candidate_ready[
decode_req_pool_indices
]
force_single_token = mask.squeeze(1) & ~remote_candidate_ready
if width > 1:
tail = runtime_states.future_input_map[decode_req_pool_indices, 1:]
dummy_tail = ids.expand(-1, width - 1)
runtime_states.future_input_map[decode_req_pool_indices, 1:] = (
torch.where(force_single_token.unsqueeze(1), dummy_tail, tail)
)
self.force_single_token_verify_buf[
row_offset : row_offset + expected_count
] = force_single_token
runtime_states.remote_spec_candidate_ready[decode_req_pool_indices] = False
# Decode-only fast path: one fused Triton kernel writes out_cache_loc,
# positions, and seq_lens in a single launch and reads
# valid_cache_lengths[pool_idx] directly, so the indexSelect + cumsum
# path + compute_position + seq_lens add are all gone.
if num_extends == 0 and batch_size > 0:
fused_decode_input_prep(
out_cache_loc_ptr=self.out_cache_loc_buf[:total_tokens],
positions_ptr=self.positions_buf[:total_tokens],
seq_lens_out_ptr=self.seq_lens_buf[:batch_size],
req_pool_indices=req_pool_indices_device,
valid_cache_lengths=runtime_states.valid_cache_lengths,
uniform_input_length=total_tokens // batch_size,
req_to_pages=req_to_page,
page_size=self.page_size,
)
# Decode path's seq_lens / positions / out_cache_loc are done.
valid_cache_lengths = None
else:
# Mixed / pure-prefill: keep the per-kernel pipeline. indexSelect
# for valid_cache_lengths is required because compute_position and
# the seq_lens add use it.
valid_cache_lengths = runtime_states.valid_cache_lengths.index_select(
0, req_pool_indices_device
)
compute_out_cache_loc(
out_cache_loc_ptr=self.out_cache_loc_buf[:total_tokens],
req_pool_indices=req_pool_indices_device,
input_lengths=input_lengths_device,
cache_start=valid_cache_lengths,
req_to_pages=req_to_page,
page_size=self.page_size,
)
# Compute positions. In mixed batches, prefill rows use their extend
# prefix lengths while decode rows use the current valid cache lengths.
prefill_prefix_lens = self.extend_prefix_lens_buf[:num_extends]
if num_extends == batch_size:
prefix_lens = prefill_prefix_lens
else:
prefix_lens = valid_cache_lengths.clone()
prefix_lens[:num_extends].copy_(prefill_prefix_lens)
# Write positions directly into the persistent buffer to skip the
# otherwise-required DtoD copy.
compute_position_triton(
extend_prefix_lens=prefix_lens,
extend_seq_lens=input_lengths_device,
extend_seq_lens_sum=total_tokens,
out=self.positions_buf[:total_tokens],
)
# Determine input_ids and forward_mode
if num_extends > 0:
prefill_token_count = sum(forward_op.input_lengths[:num_extends])
input_ids_cpu = torch.tensor(
forward_op.input_ids, device="cpu", pin_memory=True
)
self.input_ids_buf[:prefill_token_count].copy_(
input_ids_cpu,
non_blocking=True,
)
shifted_ids_cpu = torch.tensor(
forward_op.shifted_input_ids, device="cpu", pin_memory=True
)
self.shifted_prefill_ids_buf[:prefill_token_count].copy_(
shifted_ids_cpu,
non_blocking=True,
)
if num_extends < batch_size:
decode_req_pool_indices = req_pool_indices_device[
num_extends:batch_size
]
if decode_input_ids is not None:
write_decode_input_ids(
decode_req_pool_indices,
decode_input_ids,
num_extends,
batch_size - num_extends,
"mixed forward",
)
decode_ids = runtime_states.future_input_map[
decode_req_pool_indices
].flatten()
self.input_ids_buf[prefill_token_count:total_tokens].copy_(
decode_ids,
non_blocking=True,
)
self.shifted_prefill_ids_buf[prefill_token_count:total_tokens].copy_(
decode_ids,
non_blocking=True,
)
else:
# If the scheduler provides explicit decode input ids (!= -1), write
# them into future_input_map before reading, so that they take effect
# as the input for this decode step.
if decode_input_ids is not None:
write_decode_input_ids(
req_pool_indices_device,
decode_input_ids,
0,
batch_size,
"decode forward",
)
self.input_ids_buf[:total_tokens].copy_(
runtime_states.future_input_map[req_pool_indices_device].flatten(),
non_blocking=True,
)
# Defensive clamp into the valid vocab range. The decode input ids come
# from future_input_map, written by the previous iteration's
# sampler/drafter; the intermittent spec-decode decode-state race can
# surface a stale/corrupt out-of-range id there. Feeding an out-of-range
# id to the captured graph's embedding gather trips a device-side assert
# (`vectorized_gather_kernel index out of bounds`) that tears the whole
# server down. Clamp the active prefix before the graph reads these
# buffers (a no-op for legitimate ids). Mirrors the post-graph
# output_tokens clamp in the output_d2h step of
# ModelExecutor.execute_forward_op.
vocab_size = runtime_states.vocab_size
self.input_ids_buf[:total_tokens].clamp_(0, vocab_size - 1)
self.shifted_prefill_ids_buf[:total_tokens].clamp_(0, vocab_size - 1)
if valid_cache_lengths is not None:
torch.add(
input_lengths_device,
valid_cache_lengths,
out=self.seq_lens_buf[:batch_size],
)
# Refresh the padding tail of the persistent buffers every iteration.
# The captured graph replays at a padded batch size and DOES read the
# padded rows; a previous iter with a *larger* total_tokens / batch_size
# leaves stale values in the tail (real cache locations, per-request seq
# lengths, positions, token ids, req-pool slots). Reusing those for
# padded tokens routes KV writes into real cache slots (corruption),
# forces attention to scan oversize ranges, and -- for a stale
# out-of-range token id -- trips the embedding gather's device-side
# assert that tears the server down. The __init__ safe defaults
# (input_ids=1, req_pool=0, positions=0) are not enough on their own
# once a larger iter has overwritten the tail, so scrub it back here
# (cheap tail-only fills; the active prefix was written above).
if total_tokens < self.max_num_tokens:
self.input_ids_buf[total_tokens:].fill_(1)
self.out_cache_loc_buf[total_tokens:].fill_(self.dummy_kv_slot)
self.positions_buf[total_tokens:].fill_(0)
self.mrope_positions_buf[:, total_tokens:].zero_()
if batch_size < self.max_bs:
self.req_pool_indices_buf[batch_size:].fill_(0)
self.state_write_req_pool_indices_buf[batch_size:].fill_(
self.state_write_padding_pool_index
)
self.seq_lens_buf[batch_size:].fill_(1)
if (
self.has_mamba
and hasattr(forward_op, "mamba_pool_indices")
and forward_op.mamba_pool_indices
):
self._mamba_pool_indices_cpu[:batch_size].copy_(
torch.as_tensor(forward_op.mamba_pool_indices, dtype=torch.int32)
)
self._mamba_cow_src_indices_cpu[:batch_size].copy_(
torch.as_tensor(forward_op.mamba_cow_src_indices, dtype=torch.int32)
)
self._mamba_branching_seqlens_cpu[:batch_size].copy_(
torch.as_tensor(forward_op.mamba_branching_seqlens, dtype=torch.int32)
)
self._mamba_track_pool_indices_cpu[:batch_size].copy_(
torch.as_tensor(forward_op.mamba_track_pool_indices, dtype=torch.int32)
)
self.mamba_pool_indices_buf[:batch_size].copy_(
self._mamba_pool_indices_cpu[:batch_size], non_blocking=True
)
self.mamba_cow_src_indices_buf[:batch_size].copy_(
self._mamba_cow_src_indices_cpu[:batch_size], non_blocking=True
)
self.mamba_branching_seqlens_buf[:batch_size].copy_(
self._mamba_branching_seqlens_cpu[:batch_size], non_blocking=True
)
self.mamba_track_pool_indices_buf[:batch_size].copy_(
self._mamba_track_pool_indices_cpu[:batch_size], non_blocking=True
)
if batch_size < self.mamba_pool_indices_buf.shape[0]:
self.mamba_pool_indices_buf[batch_size:].fill_(-1)
self.mamba_cow_src_indices_buf[batch_size:].fill_(-1)
self.mamba_branching_seqlens_buf[batch_size:].fill_(-1)
self.mamba_track_pool_indices_buf[batch_size:].fill_(-1)
return decode_input_ids
def fill_dummy_decode_buffers(self, batch_size: int, total_tokens: int):
"""Prepare padded decode graph inputs for a rank with no real tokens."""
if total_tokens > 0:
self.input_ids_buf[:total_tokens].fill_(1)
self.out_cache_loc_buf[:total_tokens].fill_(self.dummy_kv_slot)
self.positions_buf[:total_tokens].fill_(0)
self.mrope_positions_buf[:, :total_tokens].zero_()
if batch_size > 0:
self.req_pool_indices_buf[:batch_size].fill_(0)
self.state_write_req_pool_indices_buf[:batch_size].fill_(
self.state_write_padding_pool_index
)
# seq_lens must be >= spec_num_tokens so the drafter's prewrite
# correction never goes negative.
num_tokens_per_req = total_tokens // batch_size if batch_size > 0 else 1
self.seq_lens_buf[:batch_size].fill_(max(num_tokens_per_req, 1))