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
454 lines
21 KiB
Python
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))
|