"""Lockstep batched branch decoding for the Transformers backend.""" from __future__ import annotations from collections import deque from collections.abc import Iterator from dataclasses import dataclass, field from typing import Any, Protocol import torch import torch.nn.functional as F class _Engine(Protocol): model: Any processor: Any tokenizer: Any device: torch.device eos_token_id: int fork_token_map: dict[int, list[int]] max_new_tokens: int max_branch_tokens: int max_concurrent_branches: int max_total_branches: int strict: bool _branch_forbidden_ids: list[int] def _position_ids( self, logical_positions: torch.LongTensor, *, multimodal: bool, ) -> torch.LongTensor: ... def _next_token(self, logits: torch.Tensor, *, branch: bool) -> int: ... CacheState = list[dict[str, Any]] @dataclass(slots=True) class _Stream: past: CacheState cache_len: int next_input: int | None branch_index: int | None = None fork_position: int | None = None injected_tokens: list[int] = field(default_factory=list) generated_tokens: list[int] = field(default_factory=list) state: str = "active" def _cache_to_state(cache: Any) -> CacheState: """Extract Qwen DynamicCache tensors into independently batchable state.""" try: from transformers.cache_utils import LinearAttentionLayer except ImportError as exc: # pragma: no cover - pinned Transformers provides it raise RuntimeError("Parallel decoding requires Transformers DynamicCache support.") from exc if not hasattr(cache, "layers"): raise TypeError( "Parallel decoding requires a Transformers DynamicCache; " f"received {type(cache).__name__}." ) state: CacheState = [] for layer in cache.layers: if isinstance(layer, LinearAttentionLayer): if not layer.is_conv_states_initialized or not layer.is_recurrent_states_initialized: raise RuntimeError("A linear-attention cache layer was not initialized by prefill.") state.append( { "kind": "linear", "conv": layer.conv_states.contiguous(), "recur": layer.recurrent_states.contiguous(), "has_previous_state": bool(layer.has_previous_state), } ) continue keys = getattr(layer, "keys", None) values = getattr(layer, "values", None) if keys is None or values is None: raise TypeError( "Parallel decoding only supports initialized dynamic or linear cache layers; " f"received {type(layer).__name__}." ) state.append( { "kind": "full", "keys": keys.contiguous(), "values": values.contiguous(), } ) return state def _clone_state(state: CacheState, *, truncate_to: int | None = None) -> CacheState: cloned: CacheState = [] for layer in state: if layer["kind"] == "full": keys = layer["keys"] values = layer["values"] if truncate_to is not None: keys = keys[:, :, :truncate_to, :] values = values[:, :, :truncate_to, :] cloned.append( { "kind": "full", "keys": keys.contiguous().clone(), "values": values.contiguous().clone(), } ) else: cloned.append( { "kind": "linear", "conv": layer["conv"].contiguous().clone(), "recur": layer["recur"].contiguous().clone(), "has_previous_state": layer["has_previous_state"], } ) return cloned def _state_to_cache(state: CacheState): from transformers.cache_utils import DynamicCache, DynamicLayer, LinearAttentionLayer cache = DynamicCache() layers = [] for entry in state: if entry["kind"] == "full": layer = DynamicLayer() layer.update(entry["keys"], entry["values"]) else: layer = LinearAttentionLayer() conv = entry["conv"] recur = entry["recur"] layer.lazy_initialization(conv_states=conv, recurrent_states=recur) layer.conv_states.copy_(conv) layer.recurrent_states.copy_(recur) layer.has_previous_state = entry["has_previous_state"] layers.append(layer) cache.layers = layers return cache def _batch_cache(streams: list[_Stream], max_len: int): from transformers.cache_utils import DynamicCache, DynamicLayer, LinearAttentionLayer cache = DynamicCache() layers = [] for layer_index in range(len(streams[0].past)): first = streams[0].past[layer_index] if first["kind"] == "full": keys = [] values = [] for stream in streams: entry = stream.past[layer_index] pad = max_len - stream.cache_len keys.append(F.pad(entry["keys"], (0, 0, pad, 0))) values.append(F.pad(entry["values"], (0, 0, pad, 0))) layer = DynamicLayer() layer.update(torch.cat(keys, dim=0), torch.cat(values, dim=0)) else: conv = torch.cat([stream.past[layer_index]["conv"] for stream in streams], dim=0) recur = torch.cat( [stream.past[layer_index]["recur"] for stream in streams], dim=0 ) layer = LinearAttentionLayer() layer.lazy_initialization(conv_states=conv, recurrent_states=recur) layer.conv_states.copy_(conv) layer.recurrent_states.copy_(recur) layer.has_previous_state = any( stream.past[layer_index]["has_previous_state"] for stream in streams ) layers.append(layer) cache.layers = layers return cache def _split_batch_cache(cache: Any, streams: list[_Stream]) -> None: from transformers.cache_utils import LinearAttentionLayer new_lengths = [stream.cache_len + 1 for stream in streams] for batch_index, stream in enumerate(streams): state: CacheState = [] for layer in cache.layers: if isinstance(layer, LinearAttentionLayer): state.append( { "kind": "linear", "conv": layer.conv_states[batch_index : batch_index + 1].contiguous(), "recur": layer.recurrent_states[ batch_index : batch_index + 1 ].contiguous(), "has_previous_state": bool(layer.has_previous_state), } ) continue length = new_lengths[batch_index] state.append( { "kind": "full", "keys": layer.keys[ batch_index : batch_index + 1, :, -length:, : ].contiguous(), "values": layer.values[ batch_index : batch_index + 1, :, -length:, : ].contiguous(), } ) stream.past = state stream.cache_len = new_lengths[batch_index] def _scheduler_event( main: _Stream, branches: list[_Stream], *, phase: str, batch_size: int = 0, ) -> dict[str, Any]: return { "type": "scheduler", "execution_mode": "parallel", "phase": phase, "main_active": main.state == "active", "active_branches": sum(branch.state == "active" for branch in branches), "queued_branches": sum(branch.state == "queued" for branch in branches), "completed_branches": sum(branch.state == "done" for branch in branches), "batch_size": batch_size, } def stream_parallel(engine: _Engine, messages: list[dict[str, Any]]) -> Iterator[dict[str, Any]]: """Decode main and active branches together in a per-step GPU batch.""" with torch.inference_mode(): yield from _stream_parallel(engine, messages) def _stream_parallel(engine: _Engine, messages: list[dict[str, Any]]) -> Iterator[dict[str, Any]]: yield {"type": "accepted", "execution_mode": "parallel"} inputs = engine.processor.apply_chat_template( messages, tokenize=True, add_generation_prompt=True, return_dict=True, return_tensors="pt", ).to(engine.device) prompt_ids = inputs["input_ids"][0].tolist() prompt_length = len(prompt_ids) multimodal = inputs.get("image_grid_thw") is not None or inputs.get("video_grid_thw") is not None output = engine.model(**inputs, use_cache=True) first_token = engine._next_token(output.logits, branch=False) main = _Stream( past=_cache_to_state(output.past_key_values), cache_len=prompt_length, next_input=first_token, generated_tokens=[first_token], ) del output if first_token == engine.eos_token_id or engine.max_new_tokens == 1: main.state = "done" branches: list[_Stream] = [] pending: deque[int] = deque() active: set[int] = set() dropped_triggers = 0 peak_batch_size = 1 yield _scheduler_event(main, branches, phase="prefill_complete", batch_size=1) yield { "type": "main", "token_ids": [first_token], "delta_text": engine.tokenizer.decode([first_token], skip_special_tokens=False), "total": 1, } if main.state == "done": yield {"type": "main_done", "total": 1} def register_fork(token_id: int) -> dict[str, Any] | None: nonlocal dropped_triggers injected = engine.fork_token_map.get(token_id) if not injected: return None if len(branches) >= engine.max_total_branches: dropped_triggers += 1 return None fork_position = main.cache_len expected_position = prompt_length + len(main.generated_tokens) - 1 if fork_position != expected_position: raise RuntimeError( "Main cache is not aligned with its fork token: " f"cache={fork_position}, token={expected_position}." ) branch_index = len(branches) injected_tokens = list(injected) if not injected_tokens: raise ValueError("A fork target must contain at least one token.") branches.append( _Stream( past=_clone_state(main.past, truncate_to=fork_position), cache_len=fork_position, next_input=None, branch_index=branch_index, fork_position=fork_position, injected_tokens=injected_tokens, state="queued", ) ) pending.append(branch_index) return { "type": "fork", "branch_index": branch_index, "fork_position": fork_position, "trigger_token_id": token_id, "injected_token_ids": injected_tokens, "injected_text": engine.tokenizer.decode( injected_tokens, skip_special_tokens=False ), "branch_state": "queued", } fork_event = register_fork(first_token) if fork_event is not None: yield fork_event def activate_pending() -> Iterator[dict[str, Any]]: while pending and len(active) < engine.max_concurrent_branches: branch_index = pending.popleft() branch = branches[branch_index] branch.state = "active" active.add(branch_index) injected = torch.tensor( [branch.injected_tokens], dtype=torch.long, device=engine.device ) logical = torch.arange( branch.fork_position, branch.fork_position + len(branch.injected_tokens), dtype=torch.long, device=engine.device, ).unsqueeze(0) output = engine.model( input_ids=injected, position_ids=engine._position_ids(logical, multimodal=multimodal), past_key_values=_state_to_cache(branch.past), use_cache=True, ) branch.past = _cache_to_state(output.past_key_values) branch.cache_len += len(branch.injected_tokens) first_branch_token = engine._next_token(output.logits, branch=True) branch.generated_tokens.append(first_branch_token) branch.next_input = first_branch_token del output yield { "type": "branch", "branch_index": branch_index, "fork_position": branch.fork_position, "token_ids": [first_branch_token], "delta_text": engine.tokenizer.decode( [first_branch_token], skip_special_tokens=False ), "total": len(branch.injected_tokens) + 1, } if ( first_branch_token == engine.eos_token_id or len(branch.generated_tokens) >= engine.max_branch_tokens ): branch.state = "done" active.discard(branch_index) yield { "type": "branch_done", "branch_index": branch_index, "total": len(branch.injected_tokens) + len(branch.generated_tokens), } yield _scheduler_event(main, branches, phase="branch_started") yield from activate_pending() while main.state == "active" or active or pending: yield from activate_pending() streams: list[_Stream] = [] if main.state == "active": streams.append(main) streams.extend(branches[index] for index in sorted(active)) if not streams: continue batch_size = len(streams) peak_batch_size = max(peak_batch_size, batch_size) yield _scheduler_event(main, branches, phase="decoding", batch_size=batch_size) max_len = max(stream.cache_len for stream in streams) input_ids = torch.tensor( [[stream.next_input] for stream in streams], dtype=torch.long, device=engine.device, ) logical_positions = torch.tensor( [[stream.cache_len] for stream in streams], dtype=torch.long, device=engine.device, ) attention_mask = torch.zeros( len(streams), max_len + 1, dtype=torch.long, device=engine.device ) for row, stream in enumerate(streams): attention_mask[row, max_len - stream.cache_len :] = 1 output = engine.model( input_ids=input_ids, attention_mask=attention_mask, position_ids=engine._position_ids(logical_positions, multimodal=multimodal), past_key_values=_batch_cache(streams, max_len), use_cache=True, ) logits = output.logits[:, -1, :] if engine.strict and engine._branch_forbidden_ids: logits = logits.clone() forbidden = torch.tensor( engine._branch_forbidden_ids, dtype=torch.long, device=logits.device ) for row, stream in enumerate(streams): if stream.branch_index is not None: logits[row, forbidden] = torch.finfo(logits.dtype).min token_ids = logits.argmax(dim=-1).cpu().tolist() _split_batch_cache(output.past_key_values, streams) del output, logits new_forks: list[dict[str, Any]] = [] for stream, token_id_raw in zip(streams, token_ids, strict=True): token_id = int(token_id_raw) stream.generated_tokens.append(token_id) stream.next_input = token_id if stream.branch_index is None: yield { "type": "main", "token_ids": [token_id], "delta_text": engine.tokenizer.decode( [token_id], skip_special_tokens=False ), "total": len(main.generated_tokens), } fork_event = register_fork(token_id) if fork_event is not None: new_forks.append(fork_event) if ( token_id == engine.eos_token_id or len(main.generated_tokens) >= engine.max_new_tokens ): main.state = "done" yield {"type": "main_done", "total": len(main.generated_tokens)} continue branch_index = stream.branch_index yield { "type": "branch", "branch_index": branch_index, "fork_position": stream.fork_position, "token_ids": [token_id], "delta_text": engine.tokenizer.decode([token_id], skip_special_tokens=False), "total": len(stream.injected_tokens) + len(stream.generated_tokens), } if ( token_id == engine.eos_token_id or len(stream.generated_tokens) >= engine.max_branch_tokens ): stream.state = "done" active.discard(branch_index) yield { "type": "branch_done", "branch_index": branch_index, "total": len(stream.injected_tokens) + len(stream.generated_tokens), } yield from new_forks result_branches = [] for branch in branches: token_ids = branch.injected_tokens + branch.generated_tokens result_branches.append( { "branch_index": branch.branch_index, "fork_position": branch.fork_position, "injected_token_ids": branch.injected_tokens, "token_ids": token_ids, "text": engine.tokenizer.decode(token_ids, skip_special_tokens=False), } ) yield { "type": "done", "execution_mode": "parallel", "main": engine.tokenizer.decode(main.generated_tokens, skip_special_tokens=False), "main_token_ids": main.generated_tokens, "prompt_tokens": prompt_length, "branches": result_branches, "branch_limit_reached": dropped_triggers > 0, "dropped_branch_triggers": dropped_triggers, "peak_batch_size": peak_batch_size, }