Buckets:
| """Patch vLLM Gemma4 MTP drafting with a CUDA graph loop replay. | |
| This file is loaded by the vLLM child process through PYTHONPATH. It intentionally | |
| does not patch PLE. Pupa's serve.py patches PLE textfast and scale-folds through | |
| the installed vLLM source so the fold can be verified fail-closed at load time. | |
| """ | |
| from __future__ import annotations | |
| import importlib.abc | |
| import importlib.util | |
| import os | |
| import sys | |
| from copy import copy | |
| from typing import Any | |
| LOOPGRAPH_TARGET = "vllm.v1.spec_decode.gemma4" | |
| RUNNER_TARGET = "vllm.v1.worker.gpu_model_runner" | |
| TOP_TOKEN_TARGET = "vllm.model_executor.models.gemma4_mtp" | |
| PROPOSER_TARGET = "vllm.v1.spec_decode.llm_base_proposer" | |
| REJECTION_TARGET = "vllm.v1.sample.rejection_sampler" | |
| LOOPGRAPH_WARMUP_CALLS = int(os.environ.get("LOOPGRAPH_WARMUP_CALLS", "48")) | |
| LOOPGRAPH_REQUIRE_CAPTURE = os.environ.get("LOOPGRAPH_REQUIRE_CAPTURE") == "1" | |
| LOOPGRAPH_PINGPONG_SLOTS = max(1, int(os.environ.get("LOOPGRAPH_PINGPONG_SLOTS", "1"))) | |
| LOOPGRAPH_CLEAR_STALE_SLOT_EVENTS = ( | |
| os.environ.get("LOOPGRAPH_CLEAR_STALE_SLOT_EVENTS", "1") == "1" | |
| ) | |
| FUSED_GREEDY_REJECTION_PREP = os.environ.get("FUSED_GREEDY_REJECTION_PREP", "1") == "1" | |
| FUSED_SPEC_DECODE_METADATA = os.environ.get("FUSED_SPEC_DECODE_METADATA", "1") == "1" | |
| FUSED_SPEC_DECODE_METADATA_REQUIRE = ( | |
| os.environ.get("FUSED_SPEC_DECODE_METADATA_REQUIRE") == "1" | |
| ) | |
| FUSED_DIRECT_GREEDY_REJECTION = ( | |
| os.environ.get("FUSED_DIRECT_GREEDY_REJECTION", "1") == "1" | |
| ) | |
| FUSED_DIRECT_GREEDY_REJECTION_REQUIRE = ( | |
| os.environ.get("FUSED_DIRECT_GREEDY_REJECTION_REQUIRE") == "1" | |
| ) | |
| SPEC_ASSUME_DETERMINISTIC_REJECTION = ( | |
| os.environ.get("SPEC_ASSUME_DETERMINISTIC_REJECTION", "0") == "1" | |
| ) | |
| SPEC_ASSUME_ARGMAX_PROCESSORS_OK = ( | |
| os.environ.get("SPEC_ASSUME_ARGMAX_PROCESSORS_OK", "0") == "1" | |
| ) | |
| # onegraph (@blake-fable5-1): the Gemma4 MTP drafter is Q-only and KV-shared — | |
| # it never writes KV and has no cross-position dependencies, so the padded | |
| # width-(K+1) first pass (and the full-prompt-width drafter pass on the first | |
| # decode after prefill) only ever contributes the single position selected by | |
| # token_indices_to_sample. Width-1 is exact. With ONEGRAPH=1 the whole | |
| # propose() becomes one CUDA-graph replay of K width-1 iterations: iteration 0 | |
| # consumes next_token_ids + gathered target hidden/position, iterations 1..K-1 | |
| # are the stock loopgraph body. Drafter-only => cannot change emitted tokens. | |
| ONEGRAPH = os.environ.get("ONEGRAPH", "1") == "1" | |
| FUSED_SPARSE_ARGMAX = os.environ.get("FUSED_SPARSE_ARGMAX", "1") == "1" | |
| FUSED_SPARSE_ARGMAX_REQUIRE = os.environ.get("FUSED_SPARSE_ARGMAX_REQUIRE") == "1" | |
| FUSED_SPARSE_ARGMAX_BLOCK = int(os.environ.get("FUSED_SPARSE_ARGMAX_BLOCK", "16")) | |
| _FUSED_SPARSE_ARGMAX_KERNELS: Any | None = None | |
| _FUSED_GREEDY_REJECTION_PREP_KERNEL: Any | None = None | |
| _FUSED_SPEC_DECODE_METADATA_KERNEL: Any | None = None | |
| _LOOPGRAPH_SLOT_EVENTS_BY_PTR: dict[int, Any] = {} | |
| _LOOPGRAPH_SLOT_EVENT_RECORDED_BY_PTR: dict[int, bool] = {} | |
| _PUPA_GREEDY_PREP_CACHE: dict[int, tuple[Any, Any]] = {} | |
| SPEC_ACCEPT_HISTOGRAM = os.environ.get("SPEC_ACCEPT_HISTOGRAM", "0") == "1" | |
| SPEC_TREE_SPEC = os.environ.get("SPEC_TREE_SPEC", "1") == "1" | |
| SPEC_TREE_WIDTH = int(os.environ.get("SPEC_TREE_WIDTH", "8")) | |
| SPEC_TOPK_SALVAGE_PROBE = os.environ.get("SPEC_TOPK_SALVAGE_PROBE", "0") == "1" | |
| SPEC_TOPK_SALVAGE_K = max(1, int(os.environ.get("SPEC_TOPK_SALVAGE_K", "8"))) | |
| SPEC_TOPK_SALVAGE_MAX_STEPS = int(os.environ.get("SPEC_TOPK_SALVAGE_MAX_STEPS", "4096")) | |
| SPEC_DRAFT_TOPK_PROBE = os.environ.get("SPEC_DRAFT_TOPK_PROBE", "0") == "1" or SPEC_TREE_SPEC | |
| SPEC_DRAFT_TOPK_REQUIRE = os.environ.get("SPEC_DRAFT_TOPK_REQUIRE", "0") == "1" | |
| SPEC_DRAFT_TOPK_K = max(SPEC_TREE_WIDTH, int(os.environ.get("SPEC_DRAFT_TOPK_K", "8"))) | |
| TREE_FAIL_ON_PROPOSER_PLACEHOLDER = ( | |
| os.environ.get("TREE_FAIL_ON_PROPOSER_PLACEHOLDER", "1") == "1" | |
| ) | |
| TREE_BRANCH_SANITY = os.environ.get("TREE_BRANCH_SANITY", "0") == "1" | |
| TREE_BRANCH_SANITY_EVERY = int(os.environ.get("TREE_BRANCH_SANITY_EVERY", "128")) | |
| TREE_DEBUG_STOP_AFTER_STEPS = int(os.environ.get("TREE_DEBUG_STOP_AFTER_STEPS", "0")) | |
| TREE_SAMPLERPREP_REQUIRE_CACHE = ( | |
| os.environ.get("TREE_SAMPLERPREP_REQUIRE_CACHE", "1") == "1" | |
| ) | |
| _ACCEPT_HIST_STATE: dict[str, Any] = {} | |
| _TOPK_SALVAGE_STATE: dict[str, Any] = {} | |
| _DRAFT_TOPK_STATE: dict[str, Any] = {} | |
| _TREE_BRANCH_SANITY_STATE: dict[str, Any] = {} | |
| _TREE_MASK_CACHE: dict[tuple[int, int], Any] = {} | |
| def _get_tree_mask(K: int, W: int, device: Any) -> Any: | |
| """Create a star tree mask: main path + W-1 branches per position.""" | |
| key = (K, W) | |
| if key in _TREE_MASK_CACHE: | |
| return _TREE_MASK_CACHE[key] | |
| import torch | |
| # Total tokens = 1 (base) + K (main) + K*(W-1) (branches) = 1 + KW | |
| N = 1 + K * W | |
| mask = torch.zeros((N, N), dtype=torch.bool, device=device) | |
| # Base token (index 0) attends only to itself | |
| mask[0, 0] = True | |
| for i in range(1, K + 1): | |
| # Main path node for pos i: index i | |
| # Branches for pos i: indices (K+1) + (i-1)*(W-1) ... (K+1) + i*(W-1) - 1 | |
| # All candidates for pos i attend to base and main path nodes 1...i-1 | |
| nodes_for_pos_i = [i] + list(range(K + 1 + (i - 1) * (W - 1), K + 1 + i * (W - 1))) | |
| for node_idx in nodes_for_pos_i: | |
| mask[node_idx, 0] = True | |
| for prev_i in range(1, i): | |
| mask[node_idx, prev_i] = True | |
| mask[node_idx, node_idx] = True | |
| _TREE_MASK_CACHE[key] = mask | |
| return mask | |
| def _call_base_propose(base_propose: Any, self: Any, kwargs: dict[str, Any]) -> Any: | |
| return base_propose(self, **kwargs) | |
| def _build_static_buffers(self: Any, state: dict[str, Any], cad: Any) -> None: | |
| import torch | |
| device = self.device | |
| token_count = ( | |
| self.num_speculative_tokens // SPEC_TREE_WIDTH | |
| if SPEC_TREE_SPEC | |
| else self.num_speculative_tokens | |
| ) | |
| state["outputs"] = [ | |
| torch.zeros((1, token_count), dtype=torch.int64, device=device) | |
| for _ in range(LOOPGRAPH_PINGPONG_SLOTS) | |
| ] | |
| state["out"] = state["outputs"][0] | |
| if SPEC_DRAFT_TOPK_PROBE: | |
| topk_width = max(SPEC_TOPK_SALVAGE_K, SPEC_DRAFT_TOPK_K) | |
| state["topk_outputs"] = [ | |
| torch.full( | |
| (token_count, topk_width), | |
| -1, | |
| dtype=torch.int64, | |
| device=device, | |
| ) | |
| for _ in range(LOOPGRAPH_PINGPONG_SLOTS) | |
| ] | |
| state["topk_out"] = state["topk_outputs"][0] | |
| state["next_slot"] = 0 | |
| state["_pupa_loopgraph_slot_events"] = [ | |
| torch.cuda.Event(blocking=False) for _ in state["outputs"] | |
| ] | |
| for output, event in zip( | |
| state["outputs"], state["_pupa_loopgraph_slot_events"], strict=True | |
| ): | |
| _LOOPGRAPH_SLOT_EVENTS_BY_PTR[output.data_ptr()] = event | |
| _LOOPGRAPH_SLOT_EVENT_RECORDED_BY_PTR[output.data_ptr()] = False | |
| state["seq_lens"] = torch.zeros_like(cad.seq_lens[:1]) | |
| state["first_input"] = torch.zeros((1,), dtype=torch.int32, device=device) | |
| state["block_tables"] = {} | |
| static_cad = copy(cad) | |
| static_cad.seq_lens = state["seq_lens"] | |
| static_cad.num_actual_tokens = 1 | |
| static_cad.max_query_len = 1 | |
| static_cad.max_seq_len = self.max_model_len | |
| static_cad.slot_mapping = self._slot_mapping_buffer[:1] | |
| static_cad.query_start_loc = self.arange[:2] | |
| per_layer_metadata = {} | |
| for group in self.draft_attn_groups: | |
| group_id = group.kv_cache_group_id | |
| source = self._per_group_block_tables.get(group_id, cad.block_table_tensor)[:1] | |
| block_size = group.get_metadata_builder().kv_cache_spec.block_size | |
| width = max(source.shape[1], -(-self.max_model_len // block_size)) | |
| static_block_table = torch.zeros((1, width), dtype=source.dtype, device=device) | |
| state["block_tables"][group_id] = static_block_table | |
| group_cad = copy(static_cad) | |
| group_cad.block_table_tensor = static_block_table | |
| metadata = group.get_metadata_builder().build_for_drafting( | |
| common_attn_metadata=group_cad, | |
| draft_index=1, | |
| ) | |
| for layer_name in group.layer_names: | |
| per_layer_metadata[layer_name] = metadata | |
| state["metadata"] = per_layer_metadata | |
| def _refresh_static_buffers(self: Any, state: dict[str, Any], cad: Any) -> None: | |
| state["seq_lens"].copy_(cad.seq_lens[:1]) | |
| for group_id, static_block_table in state["block_tables"].items(): | |
| source = self._per_group_block_tables.get(group_id, cad.block_table_tensor)[:1] | |
| width = min(source.shape[1], static_block_table.shape[1]) | |
| static_block_table[:, :width].copy_(source[:, :width]) | |
| def _run_graph_body(self: Any, state: dict[str, Any]) -> None: | |
| from vllm.config import CUDAGraphMode | |
| from vllm.forward_context import set_forward_context | |
| token_count = ( | |
| self.num_speculative_tokens // SPEC_TREE_WIDTH | |
| if SPEC_TREE_SPEC | |
| else self.num_speculative_tokens | |
| ) | |
| output = state["out"] | |
| onegraph = state.get("onegraph", False) | |
| with set_forward_context( | |
| state["metadata"], | |
| self.vllm_config, | |
| num_tokens=1, | |
| num_tokens_across_dp=None, | |
| cudagraph_runtime_mode=CUDAGraphMode.NONE, | |
| slot_mapping=self._get_slot_mapping(1), | |
| ): | |
| if onegraph: | |
| # K width-1 iterations; iteration 0 reads the target-sampled | |
| # token from first_input and writes out[0, 0]. | |
| for index in range(token_count): | |
| source = ( | |
| state["first_input"] | |
| if index == 0 | |
| else output[0, index - 1 : index] | |
| ) | |
| self.input_ids[:1].copy_(source) | |
| last_hidden, backbone_hidden = self.model( | |
| input_ids=self.input_ids[:1], | |
| positions=self._get_positions(1), | |
| inputs_embeds=None, | |
| hidden_states=self.hidden_states[:1], | |
| ) | |
| self.hidden_states[:1].copy_(backbone_hidden[:1]) | |
| token = _get_top_tokens_maybe_record( | |
| self.model, | |
| last_hidden[:1], | |
| state, | |
| index, | |
| ) | |
| output[0, index : index + 1].copy_(token) | |
| else: | |
| for index in range(token_count - 1): | |
| self.input_ids[:1].copy_(output[0, index : index + 1]) | |
| last_hidden, backbone_hidden = self.model( | |
| input_ids=self.input_ids[:1], | |
| positions=self._get_positions(1), | |
| inputs_embeds=None, | |
| hidden_states=self.hidden_states[:1], | |
| ) | |
| self.hidden_states[:1].copy_(backbone_hidden[:1]) | |
| token = _get_top_tokens_maybe_record( | |
| self.model, | |
| last_hidden[:1], | |
| state, | |
| index + 1, | |
| ) | |
| output[0, index + 1 : index + 2].copy_(token) | |
| def _get_top_tokens_maybe_record( | |
| model: Any, | |
| hidden_states: Any, | |
| state: dict[str, Any], | |
| step: int, | |
| ) -> Any: | |
| embedder = None | |
| if SPEC_DRAFT_TOPK_PROBE: | |
| topk_out = state.get("topk_out") | |
| embedder = getattr(model, "masked_embedding", None) | |
| if topk_out is not None and embedder is not None: | |
| embedder._ff_draft_topk_row = topk_out[step : step + 1] | |
| try: | |
| return model.get_top_tokens(hidden_states) | |
| finally: | |
| if SPEC_DRAFT_TOPK_PROBE and embedder is not None: | |
| embedder._ff_draft_topk_row = None | |
| def _select_loopgraph_output_slot(state: dict[str, Any]) -> Any: | |
| import torch | |
| outputs = state.get("outputs") | |
| if not outputs: | |
| return state["out"] | |
| slot_index = int(state.get("next_slot", 0)) | |
| output_slot = outputs[slot_index] | |
| event = _LOOPGRAPH_SLOT_EVENTS_BY_PTR.get(output_slot.data_ptr()) | |
| event_recorded = _LOOPGRAPH_SLOT_EVENT_RECORDED_BY_PTR.get( | |
| output_slot.data_ptr(), False | |
| ) | |
| if event is not None and event_recorded: | |
| torch.cuda.current_stream().wait_event(event) | |
| if LOOPGRAPH_CLEAR_STALE_SLOT_EVENTS: | |
| _LOOPGRAPH_SLOT_EVENT_RECORDED_BY_PTR[output_slot.data_ptr()] = False | |
| state["out"] = output_slot | |
| state["active_slot"] = slot_index | |
| state["next_slot"] = (slot_index + 1) % len(outputs) | |
| if SPEC_DRAFT_TOPK_PROBE and "topk_outputs" in state: | |
| state["topk_out"] = state["topk_outputs"][slot_index] | |
| return output_slot | |
| def _prime_loopgraph_outputs(state: dict[str, Any], first_token: Any) -> None: | |
| for output in state.get("outputs", [state["out"]]): | |
| output[0, 0:1].copy_(first_token) | |
| def _tree_guard_draft_tokens(tokens: Any, label: str) -> Any: | |
| if not SPEC_TREE_SPEC or not TREE_FAIL_ON_PROPOSER_PLACEHOLDER: | |
| return tokens | |
| try: | |
| if bool((tokens < 0).any().item()): | |
| head = tokens.reshape(-1)[: min(12, tokens.numel())].detach().cpu().tolist() | |
| raise RuntimeError( | |
| "[tree-debug] proposer produced negative tree draft tokens " | |
| f"at {label}: shape={tuple(tokens.shape)} head={head}" | |
| ) | |
| except RuntimeError: | |
| raise | |
| except Exception as exc: | |
| if SPEC_DRAFT_TOPK_REQUIRE: | |
| raise RuntimeError( | |
| f"[tree-debug] failed to inspect tree draft tokens at {label}" | |
| ) from exc | |
| return tokens | |
| def _capture_graph(self: Any, state: dict[str, Any]) -> None: | |
| import torch | |
| graphs = [] | |
| outputs = state.get("outputs", [state["out"]]) | |
| topk_outputs = state.get("topk_outputs", []) | |
| for index, output in enumerate(outputs): | |
| state["out"] = output | |
| if SPEC_DRAFT_TOPK_PROBE and topk_outputs: | |
| state["topk_out"] = topk_outputs[index] | |
| for _ in range(2): | |
| _run_graph_body(self, state) | |
| torch.cuda.synchronize() | |
| graph = torch.cuda.CUDAGraph() | |
| with torch.cuda.graph(graph): | |
| _run_graph_body(self, state) | |
| graphs.append(graph) | |
| state["graphs"] = graphs | |
| state["graph"] = graphs[0] | |
| state["out"] = state.get("outputs", [state["out"]])[0] | |
| def _is_loopgraph_eligible(self: Any, state: dict[str, Any], cad: Any) -> bool: | |
| return ( | |
| not state["failed"] | |
| and self.num_speculative_tokens > 1 | |
| and not self.parallel_drafting | |
| and not self._enable_probabilistic_draft_probs | |
| and not self.supports_mm_inputs | |
| and not self.uses_mrope | |
| and self.constant_draft_positions | |
| and cad.batch_size() == 1 | |
| ) | |
| def _raise_or_fallback(exc: Exception) -> None: | |
| if LOOPGRAPH_REQUIRE_CAPTURE: | |
| raise RuntimeError("LOOPGRAPH_REQUIRE_CAPTURE=1 but capture failed") from exc | |
| def _apply_loopgraph_patch(module: Any) -> None: | |
| import torch | |
| from vllm.forward_context import set_forward_context | |
| proposer_cls = module.Gemma4Proposer | |
| base_propose = proposer_cls.propose | |
| def propose( | |
| self: Any, | |
| target_token_ids: Any, | |
| target_positions: Any, | |
| target_hidden_states: Any, | |
| next_token_ids: Any, | |
| token_indices_to_sample: Any, | |
| common_attn_metadata: Any, | |
| sampling_metadata: Any, | |
| mm_embed_inputs: Any = None, | |
| num_rejected_tokens_gpu: Any = None, | |
| slot_mappings: Any = None, | |
| ) -> Any: | |
| kwargs = { | |
| "target_token_ids": target_token_ids, | |
| "target_positions": target_positions, | |
| "target_hidden_states": target_hidden_states, | |
| "next_token_ids": next_token_ids, | |
| "token_indices_to_sample": token_indices_to_sample, | |
| "common_attn_metadata": common_attn_metadata, | |
| "sampling_metadata": sampling_metadata, | |
| "mm_embed_inputs": mm_embed_inputs, | |
| "num_rejected_tokens_gpu": num_rejected_tokens_gpu, | |
| "slot_mappings": slot_mappings, | |
| } | |
| state = self.__dict__.setdefault( | |
| "_pupa_loopgraph", | |
| {"calls": 0, "graph": None, "failed": False}, | |
| ) | |
| if not _is_loopgraph_eligible(self, state, common_attn_metadata): | |
| return _call_base_propose(base_propose, self, kwargs) | |
| state["calls"] += 1 | |
| if state["graph"] is None and state["calls"] <= LOOPGRAPH_WARMUP_CALLS: | |
| return _call_base_propose(base_propose, self, kwargs) | |
| self._last_draft_probs = None | |
| token_count = ( | |
| self.num_speculative_tokens // SPEC_TREE_WIDTH | |
| if SPEC_TREE_SPEC | |
| else self.num_speculative_tokens | |
| ) | |
| num_tokens, token_indices_to_sample, cad = self.set_inputs_first_pass( | |
| target_token_ids=target_token_ids, | |
| next_token_ids=next_token_ids, | |
| target_positions=target_positions, | |
| target_hidden_states=target_hidden_states, | |
| token_indices_to_sample=token_indices_to_sample, | |
| cad=common_attn_metadata, | |
| num_rejected_tokens_gpu=num_rejected_tokens_gpu, | |
| ) | |
| _, per_layer_metadata = self.build_per_group_and_layer_attn_metadata(cad) | |
| cg_mode, num_input_tokens, num_tokens_across_dp = ( | |
| self._determine_batch_execution_and_padding(num_tokens) | |
| ) | |
| model_kwargs, slot_map_size = self.build_model_inputs_first_pass( | |
| num_tokens, | |
| num_input_tokens, | |
| mm_embed_inputs, | |
| ) | |
| with set_forward_context( | |
| per_layer_metadata, | |
| self.vllm_config, | |
| num_tokens=num_input_tokens, | |
| num_tokens_across_dp=num_tokens_across_dp, | |
| cudagraph_runtime_mode=cg_mode, | |
| slot_mapping=self._get_slot_mapping(slot_map_size, cad.slot_mapping), | |
| ): | |
| last_hidden, hidden = self.model(**model_kwargs) | |
| sample_hidden = last_hidden[token_indices_to_sample] | |
| positions = self.positions[token_indices_to_sample] | |
| first_hidden = hidden[token_indices_to_sample] | |
| self.positions[:1] = positions | |
| first_token, _ = self._sample_draft_tokens(sample_hidden, sampling_metadata) | |
| cad.num_actual_tokens = 1 | |
| cad.max_query_len = 1 | |
| cad.query_start_loc = self.arange[:2] | |
| cad.query_start_loc_cpu = torch.from_numpy(self.token_arange_np[:2]).clone() | |
| if num_rejected_tokens_gpu is not None: | |
| cad.seq_lens -= num_rejected_tokens_gpu | |
| cad._seq_lens_cpu = None | |
| cad._num_computed_tokens_cpu = None | |
| if state["graph"] is None and not state["failed"]: | |
| try: | |
| _build_static_buffers(self, state, cad) | |
| _refresh_static_buffers(self, state, cad) | |
| _prime_loopgraph_outputs(state, first_token) | |
| self.hidden_states[:1].copy_(first_hidden) | |
| _capture_graph(self, state) | |
| print( | |
| f"[pupa-loopgraph] captured K-1={token_count - 1} graph " | |
| f"at eligible call {state['calls']} " | |
| f"with slots={LOOPGRAPH_PINGPONG_SLOTS} (pid {os.getpid()})", | |
| file=sys.stderr, | |
| flush=True, | |
| ) | |
| except Exception as exc: | |
| state["failed"] = True | |
| state["graph"] = None | |
| print( | |
| f"[pupa-loopgraph] capture failed: {exc!r}", | |
| file=sys.stderr, | |
| flush=True, | |
| ) | |
| _raise_or_fallback(exc) | |
| if state["graph"] is not None: | |
| output_slot = _select_loopgraph_output_slot(state) | |
| _refresh_static_buffers(self, state, cad) | |
| output_slot[0, 0:1].copy_(first_token) | |
| self.hidden_states[:1].copy_(first_hidden) | |
| graphs = state.get("graphs") | |
| graph = graphs[state["active_slot"]] if graphs else state["graph"] | |
| graph.replay() | |
| if SPEC_DRAFT_TOPK_PROBE: | |
| _DRAFT_TOPK_STATE["latest_topk"] = state.get("topk_out") | |
| if SPEC_TREE_SPEC: | |
| import torch | |
| topk = state.get("topk_out") # (K, capture_width>=W) | |
| K = topk.shape[0] | |
| # emit EXACTLY SPEC_TREE_WIDTH candidates per position: | |
| # [main(K); branches grouped by position (K*(W-1))] | |
| # Use the graph body's sampled main path, not the separately | |
| # reconstructed top-k rank-0 row. This preserves the actual | |
| # drafter chain even if top-k tie order ever differs. | |
| main_path = output_slot[0, :K].to(topk.dtype) | |
| branches = topk[:, 1:SPEC_TREE_WIDTH] # W-slice fix | |
| all_tokens = torch.cat([main_path, branches.flatten()]) | |
| all_tokens = _tree_guard_draft_tokens(all_tokens, "loopgraph-tree") | |
| if not TREE_FAIL_ON_PROPOSER_PLACEHOLDER: | |
| all_tokens = torch.clamp(all_tokens, min=0) | |
| return all_tokens.unsqueeze(0) # (1, K*W) | |
| return output_slot | |
| cg_mode, input_batch_size, batch_size_dp = ( | |
| self._determine_batch_execution_and_padding(1) | |
| ) | |
| draft_tokens = [first_token] | |
| hidden_current = first_hidden | |
| loop_metadata = None | |
| for index in range(token_count - 1): | |
| input_ids = draft_tokens[-1].int() | |
| if index == 0: | |
| _, loop_metadata = self.build_per_group_and_layer_attn_metadata( | |
| cad, | |
| draft_index=1, | |
| ) | |
| self.input_ids[:1] = input_ids | |
| self.hidden_states[:1] = hidden_current | |
| kwargs = { | |
| "input_ids": self.input_ids[:input_batch_size], | |
| "positions": self._get_positions(input_batch_size), | |
| "inputs_embeds": None, | |
| "hidden_states": self.hidden_states[:input_batch_size], | |
| } | |
| with set_forward_context( | |
| loop_metadata, | |
| self.vllm_config, | |
| num_tokens=input_batch_size, | |
| num_tokens_across_dp=batch_size_dp, | |
| cudagraph_runtime_mode=cg_mode, | |
| slot_mapping=self._get_slot_mapping(input_batch_size), | |
| ): | |
| last_hidden, hidden = self.model(**kwargs) | |
| hidden_current = hidden[:1] | |
| token, _ = self._sample_draft_tokens(last_hidden[:1], sampling_metadata) | |
| draft_tokens.append(token) | |
| return torch.stack(draft_tokens, dim=1) | |
| def propose_onegraph( | |
| self: Any, | |
| target_token_ids: Any, | |
| target_positions: Any, | |
| target_hidden_states: Any, | |
| next_token_ids: Any, | |
| token_indices_to_sample: Any, | |
| common_attn_metadata: Any, | |
| sampling_metadata: Any, | |
| mm_embed_inputs: Any = None, | |
| num_rejected_tokens_gpu: Any = None, | |
| slot_mappings: Any = None, | |
| ) -> Any: | |
| kwargs = { | |
| "target_token_ids": target_token_ids, | |
| "target_positions": target_positions, | |
| "target_hidden_states": target_hidden_states, | |
| "next_token_ids": next_token_ids, | |
| "token_indices_to_sample": token_indices_to_sample, | |
| "common_attn_metadata": common_attn_metadata, | |
| "sampling_metadata": sampling_metadata, | |
| "mm_embed_inputs": mm_embed_inputs, | |
| "num_rejected_tokens_gpu": num_rejected_tokens_gpu, | |
| "slot_mappings": slot_mappings, | |
| } | |
| state = self.__dict__.setdefault( | |
| "_pupa_loopgraph", | |
| {"calls": 0, "graph": None, "failed": False, "onegraph": True}, | |
| ) | |
| if not _is_loopgraph_eligible(self, state, common_attn_metadata) or ( | |
| self.uses_xdrope_dim > 0 and self.draft_uses_xdrope_dim > 0 | |
| ): | |
| return _call_base_propose(base_propose, self, kwargs) | |
| state["calls"] += 1 | |
| if state["graph"] is None and state["calls"] <= LOOPGRAPH_WARMUP_CALLS: | |
| return _call_base_propose(base_propose, self, kwargs) | |
| self._last_draft_probs = None | |
| token_count = ( | |
| self.num_speculative_tokens // SPEC_TREE_WIDTH | |
| if SPEC_TREE_SPEC | |
| else self.num_speculative_tokens | |
| ) | |
| cad = common_attn_metadata | |
| # Index of the single position whose drafter output is consumed. | |
| sample_index = token_indices_to_sample | |
| if sample_index is None: | |
| sample_index = cad.query_start_loc[1:] - 1 | |
| # Loop-invariant metadata adjustments (mirror of the stock loop setup; | |
| # the width-(K+1) first pass is skipped entirely). | |
| cad.num_actual_tokens = 1 | |
| cad.max_query_len = 1 | |
| cad.query_start_loc = self.arange[:2] | |
| if num_rejected_tokens_gpu is not None: | |
| cad.seq_lens -= num_rejected_tokens_gpu | |
| cad._seq_lens_cpu = None | |
| cad._num_computed_tokens_cpu = None | |
| # Width-1 inputs: position + target hidden state of the sampled slot, | |
| # and the token the target just sampled. All device-side gathers. | |
| positions_1d = target_positions | |
| if self.vllm_config.model_config.uses_mrope: | |
| positions_1d = target_positions[0] | |
| self.positions[:1] = positions_1d[sample_index] | |
| self.hidden_states[:1].copy_(target_hidden_states[sample_index]) | |
| if state["graph"] is None and not state["failed"]: | |
| try: | |
| _build_static_buffers(self, state, cad) | |
| _refresh_static_buffers(self, state, cad) | |
| state["first_input"].copy_(next_token_ids[:1]) | |
| _capture_graph(self, state) | |
| # Warmup/capture runs consumed the hidden buffer; restore the | |
| # real value for the replay that serves this step. | |
| self.hidden_states[:1].copy_(target_hidden_states[sample_index]) | |
| print( | |
| f"[onegraph] captured K={token_count} width-1 propose graph " | |
| f"at eligible call {state['calls']} " | |
| f"with slots={LOOPGRAPH_PINGPONG_SLOTS} (pid {os.getpid()})", | |
| file=sys.stderr, | |
| flush=True, | |
| ) | |
| except Exception as exc: | |
| state["failed"] = True | |
| state["graph"] = None | |
| print( | |
| f"[onegraph] capture failed: {exc!r}", | |
| file=sys.stderr, | |
| flush=True, | |
| ) | |
| _raise_or_fallback(exc) | |
| if state["graph"] is not None: | |
| output_slot = _select_loopgraph_output_slot(state) | |
| _refresh_static_buffers(self, state, cad) | |
| state["first_input"].copy_(next_token_ids[:1]) | |
| graphs = state.get("graphs") | |
| graph = graphs[state["active_slot"]] if graphs else state["graph"] | |
| graph.replay() | |
| if SPEC_DRAFT_TOPK_PROBE: | |
| _DRAFT_TOPK_STATE["latest_topk"] = state.get("topk_out") | |
| if SPEC_TREE_SPEC: | |
| import torch | |
| topk = state.get("topk_out") # (K, capture_width>=W) | |
| K = topk.shape[0] | |
| # emit EXACTLY SPEC_TREE_WIDTH candidates per position: | |
| # [main(K); branches grouped by position (K*(W-1))] | |
| # Use the graph body's sampled main path, not the separately | |
| # reconstructed top-k rank-0 row. This preserves the actual | |
| # drafter chain even if top-k tie order ever differs. | |
| main_path = output_slot[0, :K].to(topk.dtype) | |
| branches = topk[:, 1:SPEC_TREE_WIDTH] # W-slice fix | |
| all_tokens = torch.cat([main_path, branches.flatten()]) | |
| all_tokens = _tree_guard_draft_tokens(all_tokens, "onegraph-tree") | |
| if not TREE_FAIL_ON_PROPOSER_PLACEHOLDER: | |
| all_tokens = torch.clamp(all_tokens, min=0) | |
| return all_tokens.unsqueeze(0) # (1, K*W) | |
| return output_slot | |
| # Eager width-1 fallback: K iterations, token-equivalent to the | |
| # captured body. Re-gather hidden (a failed capture's warmup runs | |
| # may have overwritten the buffer). | |
| self.hidden_states[:1].copy_(target_hidden_states[sample_index]) | |
| cg_mode, input_batch_size, batch_size_dp = ( | |
| self._determine_batch_execution_and_padding(1) | |
| ) | |
| _, loop_metadata = self.build_per_group_and_layer_attn_metadata( | |
| cad, | |
| draft_index=1, | |
| ) | |
| draft_tokens: list[Any] = [] | |
| for index in range(token_count): | |
| if index == 0: | |
| self.input_ids[:1] = next_token_ids[:1].int() | |
| else: | |
| self.input_ids[:1] = draft_tokens[-1].int() | |
| forward_kwargs = { | |
| "input_ids": self.input_ids[:input_batch_size], | |
| "positions": self._get_positions(input_batch_size), | |
| "inputs_embeds": None, | |
| "hidden_states": self.hidden_states[:input_batch_size], | |
| } | |
| with set_forward_context( | |
| loop_metadata, | |
| self.vllm_config, | |
| num_tokens=input_batch_size, | |
| num_tokens_across_dp=batch_size_dp, | |
| cudagraph_runtime_mode=cg_mode, | |
| slot_mapping=self._get_slot_mapping(input_batch_size), | |
| ): | |
| last_hidden, hidden = self.model(**forward_kwargs) | |
| self.hidden_states[:1] = hidden[:1] | |
| token, _ = self._sample_draft_tokens( | |
| last_hidden[:1], sampling_metadata | |
| ) | |
| draft_tokens.append(token) | |
| return torch.stack(draft_tokens, dim=1) | |
| proposer_cls.propose = propose_onegraph if ONEGRAPH else propose | |
| print( | |
| f"[pupa-loopgraph] patched Gemma4Proposer.propose in pid {os.getpid()} " | |
| f"(warmup_calls={LOOPGRAPH_WARMUP_CALLS}, " | |
| f"require_capture={LOOPGRAPH_REQUIRE_CAPTURE}, onegraph={ONEGRAPH})", | |
| file=sys.stderr, | |
| flush=True, | |
| ) | |
| def _get_spec_decode_metadata_kernel() -> Any: | |
| global _FUSED_SPEC_DECODE_METADATA_KERNEL | |
| if _FUSED_SPEC_DECODE_METADATA_KERNEL is not None: | |
| return _FUSED_SPEC_DECODE_METADATA_KERNEL | |
| import triton | |
| import triton.language as tl | |
| def _pupa_batch_one_spec_metadata_kernel( | |
| logits_indices_ptr, | |
| target_logits_indices_ptr, | |
| cu_num_draft_tokens_ptr, | |
| cu_num_sampled_tokens_ptr, | |
| bonus_logits_indices_ptr, | |
| start_offset, | |
| draft_len, | |
| sampled_len, | |
| BLOCK: tl.constexpr, | |
| ) -> None: | |
| offsets = tl.arange(0, BLOCK) | |
| tl.store( | |
| logits_indices_ptr + offsets, | |
| start_offset + offsets, | |
| mask=offsets < sampled_len, | |
| ) | |
| tl.store( | |
| target_logits_indices_ptr + offsets, | |
| offsets, | |
| mask=offsets < draft_len, | |
| ) | |
| tl.store(cu_num_draft_tokens_ptr, draft_len) | |
| tl.store(cu_num_sampled_tokens_ptr, sampled_len) | |
| tl.store(bonus_logits_indices_ptr, draft_len) | |
| _FUSED_SPEC_DECODE_METADATA_KERNEL = _pupa_batch_one_spec_metadata_kernel | |
| return _FUSED_SPEC_DECODE_METADATA_KERNEL | |
| def _get_spec_decode_metadata_state(self: Any, sampled_len: int) -> dict[str, Any]: | |
| import torch | |
| capacity = max(2, sampled_len) | |
| state = self.__dict__.setdefault("_pupa_spec_decode_metadata", {}) | |
| if int(state.get("capacity", 0)) >= capacity: | |
| return state | |
| state["capacity"] = capacity | |
| state["logits_indices"] = torch.empty( | |
| capacity, dtype=torch.int32, device=self.device | |
| ) | |
| state["target_logits_indices"] = torch.empty( | |
| capacity - 1, dtype=torch.int32, device=self.device | |
| ) | |
| state["cu_num_draft_tokens"] = torch.empty( | |
| 1, dtype=torch.int32, device=self.device | |
| ) | |
| state["cu_num_sampled_tokens"] = torch.empty( | |
| 1, dtype=torch.int32, device=self.device | |
| ) | |
| state["bonus_logits_indices"] = torch.empty( | |
| 1, dtype=torch.int32, device=self.device | |
| ) | |
| return state | |
| def _calc_spec_decode_metadata_fast_batch_one( | |
| self: Any, | |
| original_calc_spec_decode_metadata: Any, | |
| metadata_cls: Any, | |
| num_draft_tokens: Any, | |
| cu_num_scheduled_tokens: Any, | |
| ) -> Any: | |
| if not FUSED_SPEC_DECODE_METADATA: | |
| return original_calc_spec_decode_metadata( | |
| self, num_draft_tokens, cu_num_scheduled_tokens | |
| ) | |
| try: | |
| if getattr(self.device, "type", None) != "cuda": | |
| raise RuntimeError("fast spec metadata requires CUDA") | |
| if len(num_draft_tokens) != 1 or len(cu_num_scheduled_tokens) != 1: | |
| raise RuntimeError("fast spec metadata is batch-1 only") | |
| draft_len = int(num_draft_tokens[0]) | |
| # tree-v2: NO adjustment — with num_speculative_tokens=K*W the | |
| # scheduler already reports the expanded draft count and the upstream | |
| # index math is correct as-is (verified against the wheel source). | |
| if draft_len <= 0: | |
| raise RuntimeError(f"unsupported draft_len={draft_len}") | |
| sampled_len = draft_len + 1 | |
| start_offset = int(cu_num_scheduled_tokens[0]) - sampled_len | |
| if start_offset < 0: | |
| raise RuntimeError(f"invalid start_offset={start_offset}") | |
| state = _get_spec_decode_metadata_state(self, sampled_len) | |
| logits_indices = state["logits_indices"][:sampled_len] | |
| target_logits_indices = state["target_logits_indices"][:draft_len] | |
| cu_num_draft_tokens = state["cu_num_draft_tokens"] | |
| cu_num_sampled_tokens = state["cu_num_sampled_tokens"] | |
| bonus_logits_indices = state["bonus_logits_indices"] | |
| kernel = _get_spec_decode_metadata_kernel() | |
| block = _next_power_of_2(sampled_len) | |
| kernel[(1,)]( | |
| logits_indices, | |
| target_logits_indices, | |
| cu_num_draft_tokens, | |
| cu_num_sampled_tokens, | |
| bonus_logits_indices, | |
| start_offset, | |
| draft_len, | |
| sampled_len, | |
| BLOCK=block, | |
| ) | |
| draft_token_ids = self.input_ids.gpu[logits_indices[1:sampled_len]] | |
| return metadata_cls( | |
| draft_token_ids=draft_token_ids, | |
| num_draft_tokens=[draft_len], | |
| cu_num_draft_tokens=cu_num_draft_tokens, | |
| cu_num_sampled_tokens=cu_num_sampled_tokens, | |
| target_logits_indices=target_logits_indices, | |
| bonus_logits_indices=bonus_logits_indices, | |
| logits_indices=logits_indices, | |
| ) | |
| except Exception as exc: | |
| if FUSED_SPEC_DECODE_METADATA_REQUIRE: | |
| raise RuntimeError( | |
| "FUSED_SPEC_DECODE_METADATA_REQUIRE=1 but fusion failed" | |
| ) from exc | |
| if not getattr(self, "_pupa_spec_decode_metadata_warned", False): | |
| self._pupa_spec_decode_metadata_warned = True | |
| print( | |
| f"[pupa-gpumeta] falling back to upstream metadata: {exc!r}", | |
| file=sys.stderr, | |
| flush=True, | |
| ) | |
| return original_calc_spec_decode_metadata( | |
| self, num_draft_tokens, cu_num_scheduled_tokens | |
| ) | |
| def _loopgraph_should_record_copy_event( | |
| self: Any, | |
| scheduler_output: Any, | |
| zeros_only: bool, | |
| ) -> bool: | |
| if zeros_only: | |
| return False | |
| if not getattr(self, "use_async_scheduling", False): | |
| return True | |
| return bool( | |
| scheduler_output.has_structured_output_requests | |
| or self.input_batch.sampling_metadata.output_token_ids | |
| ) | |
| def _apply_loopgraph_copy_event_patch(module: Any) -> None: | |
| import torch | |
| runner_cls = module.GPUModelRunner | |
| original_copy_draft_token_ids_to_cpu = runner_cls._copy_draft_token_ids_to_cpu | |
| original_calc_spec_decode_metadata = runner_cls._calc_spec_decode_metadata | |
| def _copy_draft_token_ids_to_cpu( | |
| self: Any, | |
| scheduler_output: Any, | |
| zeros_only: bool = False, | |
| ) -> Any: | |
| draft_token_ids = getattr(self, "_draft_token_ids", None) | |
| result = original_copy_draft_token_ids_to_cpu( | |
| self, scheduler_output, zeros_only=zeros_only | |
| ) | |
| if not _loopgraph_should_record_copy_event( | |
| self, scheduler_output, zeros_only | |
| ): | |
| return result | |
| if not torch.is_tensor(draft_token_ids): | |
| return result | |
| event = _LOOPGRAPH_SLOT_EVENTS_BY_PTR.get(draft_token_ids.data_ptr()) | |
| copy_stream = getattr(self, "draft_token_ids_copy_stream", None) | |
| if event is not None and copy_stream is not None: | |
| with torch.cuda.stream(copy_stream): | |
| event.record(copy_stream) | |
| _LOOPGRAPH_SLOT_EVENT_RECORDED_BY_PTR[draft_token_ids.data_ptr()] = True | |
| return result | |
| def _calc_spec_decode_metadata( | |
| self: Any, | |
| num_draft_tokens: Any, | |
| cu_num_scheduled_tokens: Any, | |
| ) -> Any: | |
| return _calc_spec_decode_metadata_fast_batch_one( | |
| self, | |
| original_calc_spec_decode_metadata, | |
| module.SpecDecodeMetadata, | |
| num_draft_tokens, | |
| cu_num_scheduled_tokens, | |
| ) | |
| runner_cls._copy_draft_token_ids_to_cpu = _copy_draft_token_ids_to_cpu | |
| runner_cls._calc_spec_decode_metadata = _calc_spec_decode_metadata | |
| print( | |
| f"[pupa-loopgraph] patched GPUModelRunner draft-token copy events " | |
| f"and spec metadata in pid {os.getpid()} " | |
| f"(slots={LOOPGRAPH_PINGPONG_SLOTS}, " | |
| f"gpumeta={FUSED_SPEC_DECODE_METADATA})", | |
| file=sys.stderr, | |
| flush=True, | |
| ) | |
| def _get_fused_greedy_rejection_prep_kernel() -> Any: | |
| global _FUSED_GREEDY_REJECTION_PREP_KERNEL | |
| if _FUSED_GREEDY_REJECTION_PREP_KERNEL is not None: | |
| return _FUSED_GREEDY_REJECTION_PREP_KERNEL | |
| import triton | |
| import triton.language as tl | |
| def _pupa_rejection_greedy_with_prep_kernel( | |
| output_token_ids_ptr, | |
| next_token_ids_ptr, | |
| valid_sampled_tokens_count_ptr, | |
| cu_num_draft_tokens_ptr, | |
| draft_token_ids_ptr, | |
| target_argmax_ptr, | |
| bonus_token_ids_ptr, | |
| max_spec_len, | |
| ) -> None: | |
| req_idx = tl.program_id(0) | |
| start_idx = 0 | |
| if req_idx != 0: | |
| start_idx = tl.load(cu_num_draft_tokens_ptr + req_idx - 1) | |
| end_idx = tl.load(cu_num_draft_tokens_ptr + req_idx) | |
| num_draft_tokens = end_idx - start_idx | |
| rejected = False | |
| valid_count = 0 | |
| next_token_id = tl.load(bonus_token_ids_ptr + req_idx).to(tl.int32) | |
| row_offset = req_idx * (max_spec_len + 1) | |
| for pos in range(num_draft_tokens): | |
| if not rejected: | |
| draft_token_id = tl.load(draft_token_ids_ptr + start_idx + pos) | |
| target_argmax_id = tl.load(target_argmax_ptr + start_idx + pos).to( | |
| tl.int32 | |
| ) | |
| rejected = draft_token_id != target_argmax_id | |
| valid_count = pos + 1 | |
| next_token_id = target_argmax_id | |
| tl.store(output_token_ids_ptr + row_offset + pos, target_argmax_id) | |
| if not rejected: | |
| bonus_token_id = tl.load(bonus_token_ids_ptr + req_idx).to(tl.int32) | |
| valid_count = num_draft_tokens + 1 | |
| next_token_id = bonus_token_id | |
| tl.store( | |
| output_token_ids_ptr + row_offset + num_draft_tokens, | |
| bonus_token_id, | |
| ) | |
| tl.store(next_token_ids_ptr + req_idx, next_token_id) | |
| tl.store(valid_sampled_tokens_count_ptr + req_idx, valid_count) | |
| _FUSED_GREEDY_REJECTION_PREP_KERNEL = _pupa_rejection_greedy_with_prep_kernel | |
| return _FUSED_GREEDY_REJECTION_PREP_KERNEL | |
| def _has_tracked_thinking_state(sampling_metadata: Any) -> bool: | |
| holder = getattr(sampling_metadata, "thinking_budget_state_holder", None) | |
| return holder is not None and holder.has_tracked_requests() | |
| def _direct_rejection_is_deterministic(sampling_metadata: Any) -> bool: | |
| return bool( | |
| getattr(sampling_metadata, "all_greedy", False) | |
| or ( | |
| SPEC_ASSUME_DETERMINISTIC_REJECTION | |
| and not getattr(sampling_metadata, "all_random", False) | |
| ) | |
| ) | |
| def _safe_for_argmax_rejection(sampling_metadata: Any) -> bool: | |
| logitsprocs = getattr(sampling_metadata, "logitsprocs", None) | |
| non_argmax_processors = getattr(logitsprocs, "non_argmax_invariant", ()) | |
| return ( | |
| _direct_rejection_is_deterministic(sampling_metadata) | |
| and not getattr(sampling_metadata, "all_random", False) | |
| and getattr(sampling_metadata, "max_num_logprobs", None) is None | |
| and getattr(sampling_metadata, "no_penalties", False) | |
| and not getattr(sampling_metadata, "bad_words_token_ids", None) | |
| and getattr(sampling_metadata, "allowed_token_ids_mask", None) is None | |
| and (SPEC_ASSUME_ARGMAX_PROCESSORS_OK or not non_argmax_processors) | |
| and not _has_tracked_thinking_state(sampling_metadata) | |
| ) | |
| def _record_accept_histogram(valid_counts: Any) -> None: | |
| """Observation-only emitted-token histogram: accepted drafts + bonus token.""" | |
| if not SPEC_ACCEPT_HISTOGRAM: | |
| return | |
| try: | |
| import torch | |
| state = _ACCEPT_HIST_STATE | |
| hist = state.get("hist") | |
| if hist is None or hist.device != valid_counts.device: | |
| hist = torch.zeros(64, dtype=torch.int64, device=valid_counts.device) | |
| state["hist"] = hist | |
| state["ones"] = torch.ones(1, dtype=torch.int64, device=valid_counts.device) | |
| state["steps"] = 0 | |
| idx = valid_counts.long().clamp_(0, 63) | |
| ones = state["ones"] | |
| if idx.shape[0] != 1: | |
| ones = torch.ones_like(idx) | |
| hist.index_add_(0, idx, ones) | |
| state["steps"] += 1 | |
| if state["steps"] in (256, 1024) or state["steps"] % 2048 == 0: | |
| counts = hist.tolist() | |
| top = max(i for i, c in enumerate(counts) if c) if any(counts) else 0 | |
| print( | |
| f"[accept-hist] steps={state['steps']} " | |
| f"valid_counts_hist={counts[: top + 1]}", | |
| file=sys.stderr, | |
| flush=True, | |
| ) | |
| except Exception: | |
| pass | |
| def _record_topk_salvage( | |
| metadata: Any, | |
| draft_probs: Any, | |
| target_argmax: Any, | |
| valid_counts: Any, | |
| ) -> None: | |
| """Gate tree speculation by checking target-token rank at first reject.""" | |
| if not SPEC_TOPK_SALVAGE_PROBE: | |
| return | |
| state = _TOPK_SALVAGE_STATE | |
| steps = int(state.get("steps", 0)) + 1 | |
| state["steps"] = steps | |
| if SPEC_TOPK_SALVAGE_MAX_STEPS and steps > SPEC_TOPK_SALVAGE_MAX_STEPS: | |
| return | |
| try: | |
| import torch | |
| device = target_argmax.device | |
| counts = state.get("counts") | |
| if counts is None or counts.device != device: | |
| counts = torch.zeros(10, dtype=torch.int64, device=device) | |
| state["counts"] = counts | |
| batch_size = valid_counts.shape[0] | |
| starts = torch.zeros(batch_size, dtype=torch.long, device=device) | |
| if batch_size > 1: | |
| starts[1:] = metadata.cu_num_draft_tokens[:-1].long() | |
| valid_long = valid_counts.long() | |
| full_accept = valid_long > int(metadata.max_spec_len) | |
| rejected = ~full_accept | |
| counts[0] += batch_size | |
| counts[1] += full_accept.long().sum() | |
| counts[2] += rejected.long().sum() | |
| if bool(rejected.any()): | |
| row_ids = starts[rejected] + valid_long[rejected] - 1 | |
| target_rows = row_ids.clamp(0, target_argmax.shape[0] - 1) | |
| target_ids = target_argmax[target_rows].long() | |
| top_ids = None | |
| source_index = None | |
| if draft_probs is not None and draft_probs.ndim == 2: | |
| prob_rows = row_ids.clamp(0, draft_probs.shape[0] - 1) | |
| k = min(SPEC_TOPK_SALVAGE_K, draft_probs.shape[-1]) | |
| top_ids = torch.topk(draft_probs[prob_rows], k=k, dim=-1).indices.long() | |
| source_index = 8 | |
| else: | |
| draft_topk = _DRAFT_TOPK_STATE.get("latest_topk") | |
| if ( | |
| batch_size == 1 | |
| and draft_topk is not None | |
| and getattr(draft_topk, "ndim", 0) == 2 | |
| and getattr(draft_topk, "device", None) == device | |
| ): | |
| topk_rows = valid_long[rejected] - 1 | |
| topk_rows = topk_rows.clamp(0, draft_topk.shape[0] - 1) | |
| top_ids = draft_topk[topk_rows, :SPEC_TOPK_SALVAGE_K].long() | |
| source_index = 9 | |
| if top_ids is None: | |
| counts[7] += rejected.long().sum() | |
| else: | |
| valid_top_ids = top_ids[:, 0] >= 0 | |
| counts[7] += (~valid_top_ids).long().sum() | |
| if source_index is not None: | |
| counts[source_index] += valid_top_ids.long().sum() | |
| top_ids = top_ids[valid_top_ids] | |
| target_ids = target_ids[valid_top_ids] | |
| if top_ids is not None and int(top_ids.shape[0]) > 0: | |
| k = int(top_ids.shape[-1]) | |
| matches = top_ids == target_ids[:, None] | |
| counts[3] += matches[:, : min(1, k)].any(dim=1).long().sum() | |
| counts[4] += matches[:, : min(2, k)].any(dim=1).long().sum() | |
| counts[5] += matches[:, : min(4, k)].any(dim=1).long().sum() | |
| counts[6] += matches.any(dim=1).long().sum() | |
| if steps in (256, 1024) or steps % 2048 == 0: | |
| vals = counts.tolist() | |
| total, full, rejected, top1, top2, top4, topk, missing = vals[:8] | |
| source_probs, source_draft_topk = vals[8:10] | |
| denom = max(rejected, 1) | |
| print( | |
| "[topk-salvage] " | |
| f"steps={steps} total={total} full_accept={full} " | |
| f"first_reject={rejected} top1={top1} " | |
| f"top2={top2} ({top2 / denom:.4f}) " | |
| f"top4={top4} ({top4 / denom:.4f}) " | |
| f"top{SPEC_TOPK_SALVAGE_K}={topk} ({topk / denom:.4f}) " | |
| f"missing_candidates={missing} " | |
| f"source_probs={source_probs} " | |
| f"source_draft_topk={source_draft_topk}", | |
| file=sys.stderr, | |
| flush=True, | |
| ) | |
| except Exception as exc: | |
| if not state.get("warned"): | |
| state["warned"] = True | |
| print( | |
| f"[topk-salvage] disabled after error: {exc!r}", | |
| file=sys.stderr, | |
| flush=True, | |
| ) | |
| def _tree_branch_sanity_line(state: dict[str, Any]) -> str: | |
| first_reject = max(1, int(state.get("first_reject", 0))) | |
| steps = max(1, int(state.get("steps", 0))) | |
| rank2 = int(state.get("rank2", 0)) | |
| rank3 = int(state.get("rank3", 0)) | |
| rank4 = int(state.get("rank4", 0)) | |
| any_hit = int(state.get("any_hit", 0)) | |
| kernel_salv = int(state.get("kernel_salv", 0)) | |
| valid_sum = int(state.get("valid_sum", 0)) | |
| return ( | |
| f"steps={state.get('steps', 0)} full={state.get('full', 0)} " | |
| f"first_reject={state.get('first_reject', 0)} " | |
| f"neg_steps={state.get('neg_steps', 0)} errors={state.get('errors', 0)} " | |
| f"rank2={rank2} ({rank2 / first_reject:.4f}) " | |
| f"rank3={rank3} ({rank3 / first_reject:.4f}) " | |
| f"rank4={rank4} ({rank4 / first_reject:.4f}) " | |
| f"any_branch={any_hit} ({any_hit / first_reject:.4f}) " | |
| f"kernel_salv={kernel_salv} ({kernel_salv / first_reject:.4f}) " | |
| f"kernel_mismatch={state.get('kernel_mismatch', 0)} " | |
| f"mean_valid={valid_sum / steps:.3f}" | |
| ) | |
| def _record_tree_branch_sanity( | |
| target_argmax: Any, | |
| draft_token_ids: Any, | |
| K: int, | |
| W: int, | |
| salv_node: int, | |
| valid_count: int, | |
| label: str, | |
| ) -> None: | |
| if not TREE_BRANCH_SANITY: | |
| return | |
| state = _TREE_BRANCH_SANITY_STATE | |
| state["steps"] = int(state.get("steps", 0)) + 1 | |
| state["valid_sum"] = int(state.get("valid_sum", 0)) + int(valid_count) | |
| kernel_hit = bool(salv_node) | |
| if kernel_hit: | |
| state["kernel_salv"] = int(state.get("kernel_salv", 0)) + 1 | |
| try: | |
| import torch | |
| flat = draft_token_ids.reshape(-1)[: K * W].long() | |
| target = target_argmax.reshape(-1).long() | |
| if int(flat.numel()) < K * W or int(target.numel()) < K: | |
| raise RuntimeError( | |
| f"short tensors flat={int(flat.numel())} target={int(target.numel())}" | |
| ) | |
| if bool((flat < 0).any().item()): | |
| state["neg_steps"] = int(state.get("neg_steps", 0)) + 1 | |
| main = flat[:K] | |
| branches = flat[K : K + K * (W - 1)].reshape(K, W - 1) | |
| target_main = target[:K] | |
| misses = main != target_main | |
| if not bool(misses.any().item()): | |
| state["full"] = int(state.get("full", 0)) + 1 | |
| any_hit = False | |
| else: | |
| state["first_reject"] = int(state.get("first_reject", 0)) + 1 | |
| reject_idx = int(torch.nonzero(misses, as_tuple=False)[0].item()) | |
| target_id = target_main[reject_idx] | |
| hits = branches[reject_idx] == target_id | |
| any_hit = bool(hits.any().item()) | |
| if any_hit: | |
| state["any_hit"] = int(state.get("any_hit", 0)) + 1 | |
| if W >= 2 and bool(hits[0].item()): | |
| state["rank2"] = int(state.get("rank2", 0)) + 1 | |
| if W >= 3 and bool(hits[1].item()): | |
| state["rank3"] = int(state.get("rank3", 0)) + 1 | |
| if W >= 4 and bool(hits[2].item()): | |
| state["rank4"] = int(state.get("rank4", 0)) + 1 | |
| if any_hit != kernel_hit: | |
| state["kernel_mismatch"] = int(state.get("kernel_mismatch", 0)) + 1 | |
| except Exception as exc: | |
| state["errors"] = int(state.get("errors", 0)) + 1 | |
| if state["errors"] <= 3: | |
| print( | |
| f"[tree-branch-sanity] error label={label}: {exc!r}", | |
| file=sys.stderr, | |
| flush=True, | |
| ) | |
| steps = int(state.get("steps", 0)) | |
| if steps == 1 or ( | |
| TREE_BRANCH_SANITY_EVERY > 0 and steps % TREE_BRANCH_SANITY_EVERY == 0 | |
| ): | |
| print( | |
| f"[tree-branch-sanity] label={label} {_tree_branch_sanity_line(state)}", | |
| file=sys.stderr, | |
| flush=True, | |
| ) | |
| if TREE_DEBUG_STOP_AFTER_STEPS > 0 and steps >= TREE_DEBUG_STOP_AFTER_STEPS: | |
| raise RuntimeError( | |
| "[tree-debug] intentional diagnostic stop after " | |
| f"{steps} tree steps: {_tree_branch_sanity_line(state)}" | |
| ) | |
| def _can_use_direct_greedy_rejection( | |
| self: Any, | |
| metadata: Any, | |
| draft_probs: Any, | |
| logits: Any, | |
| sampling_metadata: Any, | |
| ) -> bool: | |
| return ( | |
| FUSED_DIRECT_GREEDY_REJECTION | |
| # tree-v2 v3: SPEC_TREE_SPEC allowed — the star salvage walk is | |
| # the first branch inside _direct_greedy_rejection (run5 fix) | |
| and not self.synthetic_mode | |
| and _safe_for_argmax_rejection(sampling_metadata) | |
| and logits is not None | |
| and logits.ndim == 2 | |
| and metadata.draft_token_ids.ndim == 1 | |
| and metadata.cu_num_draft_tokens.ndim == 1 | |
| and metadata.target_logits_indices.ndim == 1 | |
| and metadata.bonus_logits_indices.ndim in (1, 2) | |
| ) | |
| def _debug_direct_gate_miss( | |
| self: Any, | |
| metadata: Any, | |
| logits: Any, | |
| sampling_metadata: Any, | |
| ) -> None: | |
| if not SPEC_ACCEPT_HISTOGRAM or getattr(self, "_ff_direct_gate_debugged", False): | |
| return | |
| self._ff_direct_gate_debugged = True | |
| try: | |
| logitsprocs = getattr(sampling_metadata, "logitsprocs", None) | |
| non_argmax_processors = getattr(logitsprocs, "non_argmax_invariant", ()) | |
| print( | |
| "[accept-hist] direct gate miss " | |
| f"synthetic={getattr(self, 'synthetic_mode', None)} " | |
| f"all_greedy={getattr(sampling_metadata, 'all_greedy', None)} " | |
| f"all_random={getattr(sampling_metadata, 'all_random', None)} " | |
| f"assume_det={SPEC_ASSUME_DETERMINISTIC_REJECTION} " | |
| f"max_logprobs={getattr(sampling_metadata, 'max_num_logprobs', None)} " | |
| f"no_penalties={getattr(sampling_metadata, 'no_penalties', None)} " | |
| f"bad_words={bool(getattr(sampling_metadata, 'bad_words_token_ids', None))} " | |
| f"allowed_mask={getattr(sampling_metadata, 'allowed_token_ids_mask', None) is not None} " | |
| f"processors={bool(non_argmax_processors)} " | |
| f"assume_processors_ok={SPEC_ASSUME_ARGMAX_PROCESSORS_OK} " | |
| f"tracked_thinking={_has_tracked_thinking_state(sampling_metadata)} " | |
| f"logits_shape={None if logits is None else tuple(logits.shape)} " | |
| f"draft_ndim={getattr(metadata.draft_token_ids, 'ndim', None)} " | |
| f"cu_ndim={getattr(metadata.cu_num_draft_tokens, 'ndim', None)} " | |
| f"target_ndim={getattr(metadata.target_logits_indices, 'ndim', None)} " | |
| f"bonus_ndim={getattr(metadata.bonus_logits_indices, 'ndim', None)}", | |
| file=sys.stderr, | |
| flush=True, | |
| ) | |
| except Exception: | |
| pass | |
| def _direct_greedy_rejection( | |
| module: Any, | |
| metadata: Any, | |
| draft_probs: Any, | |
| logits: Any, | |
| sampling_metadata: Any, | |
| ) -> Any: | |
| import torch | |
| batch_size = len(metadata.num_draft_tokens) | |
| output_token_ids = torch.full( | |
| (batch_size, metadata.max_spec_len + 1), | |
| module.PLACEHOLDER_TOKEN_ID, | |
| dtype=torch.int32, | |
| device=logits.device, | |
| ) | |
| next_token_ids = torch.empty(batch_size, dtype=torch.int32, device=logits.device) | |
| valid_counts = torch.empty(batch_size, dtype=torch.int32, device=logits.device) | |
| argmax_token_ids = logits.argmax(dim=-1) | |
| target_argmax = argmax_token_ids[metadata.target_logits_indices].contiguous() | |
| bonus_token_ids = argmax_token_ids[ | |
| metadata.bonus_logits_indices.reshape(-1) | |
| ].contiguous() | |
| # tree-v2 direct path fix (cheesetaco-cdx): RejectionSampler.forward() | |
| # reaches this direct helper before the module-level rejection_sample() | |
| # wrapper below. Without this branch, SPEC_TREE_SPEC silently falls back to | |
| # linear chain rejection over the expanded [main; branches] draft layout, | |
| # so sibling salvage never runs and salvaged KV is never relocated. | |
| if SPEC_TREE_SPEC and batch_size == 1: | |
| import tree_v2_ext as _T2 | |
| K_tree = _T2.TREE_K | |
| expected = K_tree * _T2.TREE_W | |
| if K_tree > 0 and metadata.max_spec_len == K_tree * _T2.TREE_W: | |
| output_token_ids, valid_counts, salv = _T2.star_reject_from_drafts( | |
| target_argmax, | |
| metadata.draft_token_ids[: metadata.max_spec_len], | |
| bonus_token_ids.reshape(-1)[:1], | |
| K_tree, | |
| module.PLACEHOLDER_TOKEN_ID, | |
| logits.device, | |
| ) | |
| next_tokens = torch.gather( | |
| output_token_ids[0], 0, (valid_counts.to(torch.int64) - 1) | |
| ).to(torch.int32) | |
| salv_node, valid_i = int(salv[0]), int(valid_counts[0]) | |
| _record_tree_branch_sanity( | |
| target_argmax, | |
| metadata.draft_token_ids[: metadata.max_spec_len], | |
| K_tree, | |
| _T2.TREE_W, | |
| salv_node, | |
| valid_i, | |
| "direct", | |
| ) | |
| if salv_node: | |
| _T2.relocate_salvaged_kv(salv_node, K_tree) | |
| _T2.record_step(valid_i, salv_node, K_tree) | |
| _PUPA_GREEDY_PREP_CACHE[output_token_ids.data_ptr()] = ( | |
| next_tokens, | |
| valid_counts, | |
| ) | |
| _record_accept_histogram(valid_counts) | |
| return module.SamplerOutput( | |
| sampled_token_ids=output_token_ids, | |
| logprobs_tensors=None, | |
| ) | |
| tree_active = bool(getattr(_T2, "_STATE", {}).get("tree_step", False)) | |
| if _T2.TREE_REQUIRE and tree_active: | |
| raise RuntimeError( | |
| f"[tree-v2] REQUIRE: direct rejection saw max_spec_len=" | |
| f"{metadata.max_spec_len}, expected {expected}" | |
| ) | |
| kernel = _get_fused_greedy_rejection_prep_kernel() | |
| kernel[(batch_size,)]( | |
| output_token_ids, | |
| next_token_ids, | |
| valid_counts, | |
| metadata.cu_num_draft_tokens, | |
| metadata.draft_token_ids, | |
| target_argmax, | |
| bonus_token_ids, | |
| metadata.max_spec_len, | |
| ) | |
| _PUPA_GREEDY_PREP_CACHE[output_token_ids.data_ptr()] = ( | |
| next_token_ids, | |
| valid_counts, | |
| ) | |
| _record_accept_histogram(valid_counts) | |
| _record_topk_salvage(metadata, draft_probs, target_argmax, valid_counts) | |
| return module.SamplerOutput( | |
| sampled_token_ids=output_token_ids, | |
| logprobs_tensors=None, | |
| ) | |
| def _apply_fused_greedy_rejection_prep_patch(module: Any) -> None: | |
| import torch | |
| rejection_sampler_cls = module.RejectionSampler | |
| original_forward = rejection_sampler_cls.forward | |
| original_rejection_sample = module.rejection_sample | |
| def forward( | |
| self: Any, | |
| metadata: Any, | |
| draft_probs: Any, | |
| logits: Any, | |
| sampling_metadata: Any, | |
| ) -> Any: | |
| if _can_use_direct_greedy_rejection( | |
| self, | |
| metadata, | |
| draft_probs, | |
| logits, | |
| sampling_metadata, | |
| ): | |
| try: | |
| return _direct_greedy_rejection( | |
| module, | |
| metadata, | |
| draft_probs, | |
| logits, | |
| sampling_metadata, | |
| ) | |
| except Exception as exc: | |
| if FUSED_DIRECT_GREEDY_REJECTION_REQUIRE: | |
| raise RuntimeError( | |
| "FUSED_DIRECT_GREEDY_REJECTION_REQUIRE=1 but fusion failed" | |
| ) from exc | |
| if not getattr(self, "_pupa_direct_greedy_rejection_warned", False): | |
| self._pupa_direct_greedy_rejection_warned = True | |
| print( | |
| f"[pupa-directreject] falling back to upstream forward: {exc!r}", | |
| file=sys.stderr, | |
| flush=True, | |
| ) | |
| else: | |
| _debug_direct_gate_miss(self, metadata, logits, sampling_metadata) | |
| return original_forward( | |
| self, | |
| metadata, | |
| draft_probs, | |
| logits, | |
| sampling_metadata, | |
| ) | |
| def rejection_sample( | |
| draft_token_ids: Any, | |
| num_draft_tokens: list[int], | |
| max_spec_len: int, | |
| cu_num_draft_tokens: Any, | |
| draft_probs: Any, | |
| target_logits: Any, | |
| bonus_token_ids: Any, | |
| sampling_metadata: Any, | |
| synthetic_mode: bool = False, | |
| synthetic_conditional_rates: Any = None, | |
| use_fp64_gumbel: bool = False, | |
| ) -> Any: | |
| # Standard fast path checks | |
| if ( | |
| not FUSED_GREEDY_REJECTION_PREP | |
| or synthetic_mode | |
| or not _safe_for_argmax_rejection(sampling_metadata) | |
| or draft_token_ids.ndim != 1 | |
| or cu_num_draft_tokens.ndim != 1 | |
| or target_logits.ndim != 2 | |
| or not draft_token_ids.is_contiguous() | |
| or not bonus_token_ids.is_contiguous() | |
| ): | |
| # Fallback to upstream | |
| return original_rejection_sample( | |
| draft_token_ids, | |
| num_draft_tokens, | |
| max_spec_len, | |
| cu_num_draft_tokens, | |
| draft_probs, | |
| target_logits, | |
| bonus_token_ids, | |
| sampling_metadata, | |
| synthetic_mode, | |
| synthetic_conditional_rates, | |
| use_fp64_gumbel, | |
| ) | |
| batch_size = len(num_draft_tokens) | |
| num_tokens = draft_token_ids.shape[0] | |
| vocab_size = target_logits.shape[-1] | |
| device = target_logits.device | |
| # Star Tree Salvage v2 (chiku-inu) — fused walk on the pinned | |
| # layout; topk reconstructed from the scheduled drafts themselves. | |
| # Validated: tree-dev/sim_tree_identity.py (240-token greedy identity) | |
| # + tree-v2/test_ext_walk.py (2000/2000 vs that reference). | |
| if SPEC_TREE_SPEC and batch_size == 1: | |
| import tree_v2_ext as _T2 | |
| K_tree = _T2.TREE_K | |
| expected = K_tree * _T2.TREE_W | |
| if K_tree > 0 and max_spec_len == K_tree * _T2.TREE_W: | |
| target_argmax = target_logits.argmax(dim=-1) | |
| output_token_ids, valid_counts, salv = _T2.star_reject_from_drafts( | |
| target_argmax, draft_token_ids[:max_spec_len], | |
| bonus_token_ids.reshape(-1)[:1], K_tree, | |
| module.PLACEHOLDER_TOKEN_ID, device, | |
| ) | |
| next_tokens = torch.gather( | |
| output_token_ids[0], 0, (valid_counts.to(torch.int64) - 1) | |
| ).to(torch.int32) | |
| # one small host sync per step (rides with the step's existing | |
| # output transfer); relocation only on salvage steps (~30%) | |
| salv_node, valid_i = int(salv[0]), int(valid_counts[0]) | |
| _record_tree_branch_sanity( | |
| target_argmax, | |
| draft_token_ids[:max_spec_len], | |
| K_tree, | |
| _T2.TREE_W, | |
| salv_node, | |
| valid_i, | |
| "module", | |
| ) | |
| if salv_node: | |
| _T2.relocate_salvaged_kv(salv_node, K_tree) | |
| _T2.record_step(valid_i, salv_node, K_tree) | |
| _PUPA_GREEDY_PREP_CACHE[output_token_ids.data_ptr()] = ( | |
| next_tokens, valid_counts, | |
| ) | |
| return output_token_ids | |
| tree_active = bool(getattr(_T2, "_STATE", {}).get("tree_step", False)) | |
| if _T2.TREE_REQUIRE and tree_active: | |
| raise RuntimeError( | |
| f"[tree-v2] REQUIRE: rejection saw max_spec_len={max_spec_len}, " | |
| f"expected {expected}" | |
| ) | |
| # Standard fused path | |
| if target_logits.shape != (num_tokens, vocab_size): | |
| return original_rejection_sample( | |
| draft_token_ids, | |
| num_draft_tokens, | |
| max_spec_len, | |
| cu_num_draft_tokens, | |
| draft_probs, | |
| target_logits, | |
| bonus_token_ids, | |
| sampling_metadata, | |
| synthetic_mode, | |
| synthetic_conditional_rates, | |
| use_fp64_gumbel, | |
| ) | |
| output_token_ids = torch.full( | |
| (batch_size, max_spec_len + 1), | |
| module.PLACEHOLDER_TOKEN_ID, | |
| dtype=torch.int32, | |
| device=device, | |
| ) | |
| next_token_ids = torch.empty(batch_size, dtype=torch.int32, device=device) | |
| valid_counts = torch.empty(batch_size, dtype=torch.int32, device=device) | |
| target_argmax = target_logits.argmax(dim=-1) | |
| kernel = _get_fused_greedy_rejection_prep_kernel() | |
| kernel[(batch_size,)]( | |
| output_token_ids, | |
| next_token_ids, | |
| valid_counts, | |
| cu_num_draft_tokens, | |
| draft_token_ids, | |
| target_argmax, | |
| bonus_token_ids, | |
| max_spec_len, | |
| ) | |
| _PUPA_GREEDY_PREP_CACHE[output_token_ids.data_ptr()] = ( | |
| next_token_ids, | |
| valid_counts, | |
| ) | |
| _record_accept_histogram(valid_counts) | |
| return output_token_ids | |
| rejection_sampler_cls.forward = forward | |
| module.rejection_sample = rejection_sample | |
| print( | |
| f"[pupa-samplerprep] patched greedy rejection/prep fusion " | |
| f"in pid {os.getpid()} (enabled={FUSED_GREEDY_REJECTION_PREP}, " | |
| f"direct={FUSED_DIRECT_GREEDY_REJECTION})", | |
| file=sys.stderr, | |
| flush=True, | |
| ) | |
| def _apply_samplerprep_proposer_patch(module: Any) -> None: | |
| proposer_cls = module.SpecDecodeBaseProposer | |
| original_prepare_next_token_ids_padded = proposer_cls.prepare_next_token_ids_padded | |
| cache_stats = {"hit": 0, "miss_placeholder": 0} | |
| def prepare_next_token_ids_padded( | |
| self: Any, | |
| sampled_token_ids: Any, | |
| requests: dict[str, Any], | |
| gpu_input_batch: Any, | |
| discard_request_mask: Any, | |
| ) -> tuple[Any, Any]: | |
| cached = _PUPA_GREEDY_PREP_CACHE.pop(sampled_token_ids.data_ptr(), None) | |
| if ( | |
| FUSED_GREEDY_REJECTION_PREP | |
| and cached is not None | |
| and sampled_token_ids.shape[0] == gpu_input_batch.num_reqs | |
| and gpu_input_batch.num_reqs == 1 | |
| ): | |
| cache_stats["hit"] += 1 | |
| return cached | |
| if ( | |
| SPEC_TREE_SPEC | |
| and FUSED_GREEDY_REJECTION_PREP | |
| and TREE_SAMPLERPREP_REQUIRE_CACHE | |
| ): | |
| try: | |
| has_placeholder = bool((sampled_token_ids < 0).any().item()) | |
| except Exception: | |
| has_placeholder = False | |
| if has_placeholder: | |
| cache_stats["miss_placeholder"] += 1 | |
| raise RuntimeError( | |
| "tree-v2 placeholder row reached upstream " | |
| "prepare_next_token_ids_padded without a samplerprep cache " | |
| f"hit; shape={tuple(sampled_token_ids.shape)} " | |
| f"cache_stats={cache_stats}" | |
| ) | |
| return original_prepare_next_token_ids_padded( | |
| self, | |
| sampled_token_ids, | |
| requests, | |
| gpu_input_batch, | |
| discard_request_mask, | |
| ) | |
| proposer_cls.prepare_next_token_ids_padded = prepare_next_token_ids_padded | |
| print( | |
| f"[pupa-samplerprep] patched SpecDecodeBaseProposer.prepare_next_token_ids_padded " | |
| f"in pid {os.getpid()} (enabled={FUSED_GREEDY_REJECTION_PREP})", | |
| file=sys.stderr, | |
| flush=True, | |
| ) | |
| def _next_power_of_2(value: int) -> int: | |
| return 1 << (max(1, value) - 1).bit_length() | |
| def _get_fused_sparse_argmax_kernels() -> Any: | |
| global _FUSED_SPARSE_ARGMAX_KERNELS | |
| if _FUSED_SPARSE_ARGMAX_KERNELS is not None: | |
| return _FUSED_SPARSE_ARGMAX_KERNELS | |
| import triton | |
| import triton.language as tl | |
| def _sparse_argmax_blocks_kernel( | |
| hidden_states, | |
| lm_head_weight, | |
| top_centroids, | |
| token_ordering, | |
| partial_scores, | |
| partial_tokens, | |
| hidden_stride_t, | |
| hidden_stride_d, | |
| lm_head_stride_v, | |
| lm_head_stride_d, | |
| top_stride_t, | |
| top_stride_k, | |
| partial_score_stride_t, | |
| partial_token_stride_t, | |
| VOCAB_PER_CENTROID: tl.constexpr, | |
| SELECTED_COUNT: tl.constexpr, | |
| HIDDEN_SIZE: tl.constexpr, | |
| BLOCK_SELECTED: tl.constexpr, | |
| BLOCK_D: tl.constexpr, | |
| ) -> None: | |
| token_idx = tl.program_id(0) | |
| selected_block = tl.program_id(1) | |
| selected_offsets = selected_block * BLOCK_SELECTED + tl.arange( | |
| 0, BLOCK_SELECTED | |
| ) | |
| valid_selected = selected_offsets < SELECTED_COUNT | |
| centroid_slots = selected_offsets // VOCAB_PER_CENTROID | |
| token_slots = selected_offsets - centroid_slots * VOCAB_PER_CENTROID | |
| centroid_ids = tl.load( | |
| top_centroids + token_idx * top_stride_t + centroid_slots * top_stride_k, | |
| mask=valid_selected, | |
| other=0, | |
| ) | |
| vocab_ids = tl.load( | |
| token_ordering + centroid_ids * VOCAB_PER_CENTROID + token_slots, | |
| mask=valid_selected, | |
| other=0, | |
| ) | |
| d_offsets = tl.arange(0, BLOCK_D) | |
| valid_d = d_offsets < HIDDEN_SIZE | |
| hidden = tl.load( | |
| hidden_states + token_idx * hidden_stride_t + d_offsets * hidden_stride_d, | |
| mask=valid_d, | |
| other=0.0, | |
| ).to(tl.float32) | |
| weights = tl.load( | |
| lm_head_weight | |
| + vocab_ids[:, None] * lm_head_stride_v | |
| + d_offsets[None, :] * lm_head_stride_d, | |
| mask=valid_selected[:, None] & valid_d[None, :], | |
| other=0.0, | |
| ).to(tl.float32) | |
| scores = tl.sum(weights * hidden[None, :], axis=1) | |
| # The PyTorch sparse path materializes bf16 logits before argmax. | |
| scores = scores.to(tl.bfloat16).to(tl.float32) | |
| scores = tl.where(valid_selected, scores, -float("inf")) | |
| best_score, best_local_idx = tl.max( | |
| scores, | |
| axis=0, | |
| return_indices=True, | |
| return_indices_tie_break_left=True, | |
| ) | |
| best_selected = selected_block * BLOCK_SELECTED + best_local_idx | |
| best_centroid_slot = best_selected // VOCAB_PER_CENTROID | |
| best_token_slot = best_selected - best_centroid_slot * VOCAB_PER_CENTROID | |
| best_centroid = tl.load( | |
| top_centroids + token_idx * top_stride_t + best_centroid_slot * top_stride_k | |
| ) | |
| best_token = tl.load( | |
| token_ordering + best_centroid * VOCAB_PER_CENTROID + best_token_slot | |
| ) | |
| tl.store( | |
| partial_scores + token_idx * partial_score_stride_t + selected_block, | |
| best_score, | |
| ) | |
| tl.store( | |
| partial_tokens + token_idx * partial_token_stride_t + selected_block, | |
| best_token, | |
| ) | |
| def _sparse_argmax_reduce_kernel( | |
| partial_scores, | |
| partial_tokens, | |
| output_tokens, | |
| partial_score_stride_t, | |
| partial_token_stride_t, | |
| output_stride_t, | |
| NUM_BLOCKS: tl.constexpr, | |
| BLOCK_BLOCKS: tl.constexpr, | |
| ) -> None: | |
| token_idx = tl.program_id(0) | |
| block_offsets = tl.arange(0, BLOCK_BLOCKS) | |
| valid_blocks = block_offsets < NUM_BLOCKS | |
| scores = tl.load( | |
| partial_scores + token_idx * partial_score_stride_t + block_offsets, | |
| mask=valid_blocks, | |
| other=-float("inf"), | |
| ) | |
| _, best_block = tl.max( | |
| scores, | |
| axis=0, | |
| return_indices=True, | |
| return_indices_tie_break_left=True, | |
| ) | |
| token = tl.load( | |
| partial_tokens + token_idx * partial_token_stride_t + best_block | |
| ) | |
| tl.store(output_tokens + token_idx * output_stride_t, token) | |
| _FUSED_SPARSE_ARGMAX_KERNELS = ( | |
| triton, | |
| _sparse_argmax_blocks_kernel, | |
| _sparse_argmax_reduce_kernel, | |
| ) | |
| return _FUSED_SPARSE_ARGMAX_KERNELS | |
| def _fallback_sparse_argmax( | |
| self: Any, | |
| original_get_top_tokens: Any, | |
| hidden_states: Any, | |
| lm_head_weight: Any, | |
| reason: Exception, | |
| ) -> Any: | |
| if FUSED_SPARSE_ARGMAX_REQUIRE: | |
| raise RuntimeError( | |
| "FUSED_SPARSE_ARGMAX_REQUIRE=1 but fusion failed" | |
| ) from reason | |
| if not getattr(self, "_pupa_fused_sparse_argmax_warned", False): | |
| self._pupa_fused_sparse_argmax_warned = True | |
| print( | |
| f"[pupa-fused-sparse-argmax] falling back to PyTorch path: {reason!r}", | |
| file=sys.stderr, | |
| flush=True, | |
| ) | |
| return original_get_top_tokens(self, hidden_states, lm_head_weight) | |
| def _apply_fused_top_token_patch(module: Any) -> None: | |
| import torch | |
| embedder_cls = module.Gemma4MTPMaskedEmbedder | |
| original_get_top_tokens = embedder_cls.get_top_tokens | |
| def _select_and_score_unsorted(self: Any, hidden_states: Any, lm_head_weight: Any): | |
| num_tokens = hidden_states.shape[0] | |
| _, top_k_indices = torch.topk( | |
| self.centroids(hidden_states), | |
| k=self.centroid_intermediate_top_k, | |
| dim=-1, | |
| sorted=False, | |
| ) | |
| clusters = self.token_ordering.view( | |
| self.num_centroids, | |
| self.vocab_size_per_centroid, | |
| ) | |
| selected = clusters[top_k_indices] | |
| embeddings = lm_head_weight[selected.reshape(-1)].view( | |
| num_tokens, | |
| self.num_selected, | |
| self.hidden_size, | |
| ) | |
| logits = torch.einsum("td,tsd->ts", hidden_states, embeddings) | |
| return logits, selected.view(num_tokens, -1) | |
| def _record_draft_topk( | |
| self: Any, | |
| hidden_states: Any, | |
| lm_head_weight: Any, | |
| ) -> None: | |
| if not SPEC_DRAFT_TOPK_PROBE: | |
| return | |
| row = getattr(self, "_ff_draft_topk_row", None) | |
| if row is None: | |
| return | |
| try: | |
| logits, selected = _select_and_score_unsorted( | |
| self, | |
| hidden_states, | |
| lm_head_weight, | |
| ) | |
| k = min(SPEC_DRAFT_TOPK_K, int(logits.shape[-1]), int(row.shape[-1])) | |
| _, rank_ids = torch.topk(logits, k=k, dim=-1, sorted=True) | |
| token_ids = selected.long().gather(1, rank_ids.long()) | |
| row[: token_ids.shape[0], :k].copy_(token_ids.to(row.dtype)) | |
| except Exception as exc: | |
| if SPEC_DRAFT_TOPK_REQUIRE: | |
| raise RuntimeError("SPEC_DRAFT_TOPK_REQUIRE=1 but top-k failed") from exc | |
| if not getattr(self, "_ff_draft_topk_warned", False): | |
| self._ff_draft_topk_warned = True | |
| print( | |
| f"[draft-topk] disabled after error: {exc!r}", | |
| file=sys.stderr, | |
| flush=True, | |
| ) | |
| def get_top_tokens_fused(self: Any, hidden_states: Any, lm_head_weight: Any) -> Any: | |
| if not FUSED_SPARSE_ARGMAX: | |
| return original_get_top_tokens(self, hidden_states, lm_head_weight) | |
| try: | |
| if ( | |
| hidden_states.device.type != "cuda" | |
| or lm_head_weight.device.type != "cuda" | |
| ): | |
| raise RuntimeError("fusion requires CUDA tensors") | |
| if ( | |
| hidden_states.dtype != torch.bfloat16 | |
| or lm_head_weight.dtype != torch.bfloat16 | |
| ): | |
| raise RuntimeError( | |
| "fusion currently preserves exact PyTorch argmax only for bf16" | |
| ) | |
| hidden_size = int(self.hidden_size) | |
| if hidden_size <= 0 or hidden_size > 1024: | |
| raise RuntimeError(f"unsupported hidden_size={hidden_size}") | |
| _record_draft_topk(self, hidden_states, lm_head_weight) | |
| triton, blocks_kernel, reduce_kernel = _get_fused_sparse_argmax_kernels() | |
| num_tokens = int(hidden_states.shape[0]) | |
| selected_count = int(self.num_selected) | |
| block_selected = _next_power_of_2(FUSED_SPARSE_ARGMAX_BLOCK) | |
| num_blocks = triton.cdiv(selected_count, block_selected) | |
| reduce_block = _next_power_of_2(num_blocks) | |
| block_d = _next_power_of_2(hidden_size) | |
| _, top_k_indices = torch.topk( | |
| self.centroids(hidden_states), | |
| k=self.centroid_intermediate_top_k, | |
| dim=-1, | |
| sorted=False, | |
| ) | |
| partial_scores = torch.empty( | |
| (num_tokens, num_blocks), | |
| dtype=torch.float32, | |
| device=hidden_states.device, | |
| ) | |
| partial_tokens = torch.empty( | |
| (num_tokens, num_blocks), | |
| dtype=torch.int64, | |
| device=hidden_states.device, | |
| ) | |
| output_tokens = torch.empty( | |
| (num_tokens,), | |
| dtype=torch.int64, | |
| device=hidden_states.device, | |
| ) | |
| blocks_kernel[(num_tokens, num_blocks)]( | |
| hidden_states, | |
| lm_head_weight, | |
| top_k_indices, | |
| self.token_ordering, | |
| partial_scores, | |
| partial_tokens, | |
| hidden_states.stride(0), | |
| hidden_states.stride(1), | |
| lm_head_weight.stride(0), | |
| lm_head_weight.stride(1), | |
| top_k_indices.stride(0), | |
| top_k_indices.stride(1), | |
| partial_scores.stride(0), | |
| partial_tokens.stride(0), | |
| VOCAB_PER_CENTROID=int(self.vocab_size_per_centroid), | |
| SELECTED_COUNT=selected_count, | |
| HIDDEN_SIZE=hidden_size, | |
| BLOCK_SELECTED=block_selected, | |
| BLOCK_D=block_d, | |
| num_warps=8, | |
| ) | |
| reduce_kernel[(num_tokens,)]( | |
| partial_scores, | |
| partial_tokens, | |
| output_tokens, | |
| partial_scores.stride(0), | |
| partial_tokens.stride(0), | |
| output_tokens.stride(0), | |
| NUM_BLOCKS=num_blocks, | |
| BLOCK_BLOCKS=reduce_block, | |
| num_warps=8, | |
| ) | |
| return output_tokens | |
| except Exception as exc: | |
| return _fallback_sparse_argmax( | |
| self, | |
| original_get_top_tokens, | |
| hidden_states, | |
| lm_head_weight, | |
| exc, | |
| ) | |
| embedder_cls._select_and_score = _select_and_score_unsorted | |
| embedder_cls.get_top_tokens = get_top_tokens_fused | |
| print( | |
| f"[pupa-fused-sparse-argmax] patched Gemma4MTPMaskedEmbedder top-token path " | |
| f"in pid {os.getpid()} (enabled={FUSED_SPARSE_ARGMAX}, " | |
| f"require={FUSED_SPARSE_ARGMAX_REQUIRE}, block={FUSED_SPARSE_ARGMAX_BLOCK})", | |
| file=sys.stderr, | |
| flush=True, | |
| ) | |
| class _PatchingLoader(importlib.abc.Loader): | |
| def __init__(self, inner: importlib.abc.Loader, patch_fn: Any) -> None: | |
| self._inner = inner | |
| self._patch_fn = patch_fn | |
| def create_module(self, spec: Any) -> Any: | |
| return self._inner.create_module(spec) | |
| def exec_module(self, module: Any) -> None: | |
| self._inner.exec_module(module) | |
| self._patch_fn(module) | |
| class _TargetFinder(importlib.abc.MetaPathFinder): | |
| def __init__(self, target: str, patch_fn: Any) -> None: | |
| self._target = target | |
| self._patch_fn = patch_fn | |
| self._busy = False | |
| def find_spec(self, fullname: str, path: Any = None, target: Any = None) -> Any: | |
| if fullname != self._target or self._busy: | |
| return None | |
| self._busy = True | |
| try: | |
| spec = importlib.util.find_spec(fullname) | |
| finally: | |
| self._busy = False | |
| if spec is None or spec.loader is None: | |
| return None | |
| spec.loader = _PatchingLoader(spec.loader, self._patch_fn) | |
| return spec | |
| def _apply_tree_attention_patch(module: Any) -> None: | |
| """Monkey-patch Gemma4 attention to support Star Tree masking.""" | |
| import torch | |
| import functools | |
| # Gemma4 typically uses Gemma4Attention. | |
| # We use signature-neutral wrapper (*args, **kwargs) to satisfy Dynamo. | |
| for name in dir(module): | |
| attr = getattr(module, name) | |
| if (isinstance(attr, type) and "Attention" in name | |
| and hasattr(attr, "forward") and not name.startswith("_") | |
| and attr.__module__ == module.__name__): | |
| original_forward = attr.forward | |
| def forward(self: Any, *args: Any, **kwargs: Any) -> Any: | |
| # attn_metadata is usually a keyword or positional argument. | |
| metadata = kwargs.get("attn_metadata") | |
| if metadata is None: | |
| for arg in args: | |
| if hasattr(arg, "num_prefills"): | |
| metadata = arg | |
| break | |
| if (SPEC_TREE_SPEC and metadata is not None | |
| and getattr(metadata, "num_prefills", 1) == 0): | |
| num_tokens = getattr(metadata, "num_tokens", -1) | |
| # We expect 1 (base) + K*W (tree) | |
| K = int(os.environ.get("SPECULATIVE_TOKENS", "7")) | |
| W = SPEC_TREE_WIDTH | |
| if num_tokens == 1 + K * W: | |
| mask = _get_tree_mask(K, W, device=self.device) | |
| if hasattr(metadata, "attn_bias"): | |
| # vLLM V1: 0 for allow, -inf for block. | |
| bias = torch.zeros_like(mask, dtype=torch.float32) | |
| bias.masked_fill_(~mask, float("-inf")) | |
| metadata.attn_bias = bias | |
| return original_forward(self, *args, **kwargs) | |
| attr.forward = forward | |
| print(f"[tree-spec] patched {name}.forward for Star Tree verification", | |
| file=sys.stderr, flush=True) | |
| sys.meta_path.insert(0, _TargetFinder(TOP_TOKEN_TARGET, _apply_fused_top_token_patch)) | |
| sys.meta_path.insert(0, _TargetFinder(PROPOSER_TARGET, _apply_samplerprep_proposer_patch)) | |
| sys.meta_path.insert(0, _TargetFinder(REJECTION_TARGET, _apply_fused_greedy_rejection_prep_patch)) | |
| sys.meta_path.insert(0, _TargetFinder(RUNNER_TARGET, _apply_loopgraph_copy_event_patch)) | |
| sys.meta_path.insert(0, _TargetFinder(LOOPGRAPH_TARGET, _apply_loopgraph_patch)) | |
| # tree-v2: attn-bias registration removed (FA has no attn_bias; star | |
| # attention is installed by tree_v2_ext at the Attention layer instead) | |
| # PCK-04: registers Gemma4ForCausalLM lm_head rebuild + logits scatter. | |
| import serve_patch_pck04 # noqa: E402, F401 | |
| # tree-v2 machinery (kernels + runner/attention patches) | |
| import tree_v2_ext # noqa: E402, F401 | |
| import steptime_patch # noqa: E402, F401 (credit: agent-smith) | |
Xet Storage Details
- Size:
- 81.8 kB
- Xet hash:
- d59bf7417a65a265ede281407094710fe4b34c210b793e42d07a1b96099ef8c5
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.