| import pickle |
| import gc |
| import torch |
| import torch.distributed as dist |
| from multiprocessing.synchronize import Event |
| from multiprocessing.shared_memory import SharedMemory |
| import sys |
|
|
| from ..config import Config |
| from .sequence import Sequence |
| from ..layers.sampler import Sampler |
| from ..utils.context import set_context, get_context, reset_context |
|
|
| import socket |
|
|
|
|
| def find_available_port(start_port: int = 2333, max_attempts: int = 100) -> int: |
| """Find an available port starting from start_port. |
| |
| Args: |
| start_port: The starting port number to check |
| max_attempts: Maximum number of ports to try |
| |
| Returns: |
| An available port number |
| |
| Raises: |
| RuntimeError: If no available port is found within max_attempts |
| """ |
| for i in range(max_attempts): |
| port = start_port + i |
| try: |
| with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: |
| s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) |
| s.bind(('localhost', port)) |
| return port |
| except OSError: |
| |
| continue |
| raise RuntimeError(f"Could not find an available port starting from {start_port} after {max_attempts} attempts") |
|
|
|
|
| class ModelRunner: |
|
|
| def __init__(self, config: Config, rank: int, event: Event | list[Event], model_object=None, graph_pool_handle=None): |
| |
| torch._dynamo.config.capture_scalar_outputs = True |
| |
| self.config = config |
| hf_config = config.hf_config |
| self.block_size = config.kvcache_block_size |
| self.enforce_eager = config.enforce_eager |
| self.world_size = config.tensor_parallel_size |
| self.rank = rank |
| self.event = event |
| if self.world_size > 1: |
| dist_port = find_available_port() |
| print(f"[debug]dist_port: {dist_port}") |
| |
| backend = "gloo" if sys.platform == "win32" else "nccl" |
| dist.init_process_group(backend, f"tcp://127.0.0.1:{dist_port}", world_size=self.world_size, rank=rank) |
| torch.cuda.set_device(rank) |
| else: |
| if torch.cuda.is_available(): |
| torch.cuda.set_device(0) |
| default_dtype = torch.get_default_dtype() |
| |
| config_dtype = getattr(hf_config, 'dtype', getattr(hf_config, 'torch_dtype', None)) |
|
|
| |
| |
| if config_dtype is None: |
| config_dtype = torch.bfloat16 |
| elif isinstance(config_dtype, str): |
| |
| dtype_map = { |
| 'float32': torch.float32, |
| 'float16': torch.float16, |
| 'bfloat16': torch.bfloat16, |
| 'float64': torch.float64, |
| 'torch.float32': torch.float32, |
| 'torch.float16': torch.float16, |
| 'torch.bfloat16': torch.bfloat16, |
| 'torch.float64': torch.float64, |
| } |
| config_dtype = dtype_map.get(config_dtype.lower(), torch.bfloat16) |
| elif not isinstance(config_dtype, torch.dtype) or not config_dtype.is_floating_point: |
| |
| config_dtype = torch.bfloat16 |
|
|
| self.dtype = config_dtype |
| self._runtime_ready = False |
| self._graph_cache = {} |
| self._graph_cache_order = [] |
| self._logits_bias_cache = {} |
| self._sampling_generator = None |
| self._runtime_signature = None |
| self._graph_pool_seed = graph_pool_handle |
| self._guard_counts = {} |
| self._guard_seen_details = set() |
| torch.set_default_dtype(config_dtype) |
| if model_object is None: |
| raise RuntimeError( |
| "nanovllm now requires a preloaded MMGP model object. " |
| "Pass model_object=... when creating LLM." |
| ) |
| self.model = model_object |
| self.sampler = Sampler() |
| |
| |
| |
| self._allocate_sample_buffers() |
| |
| torch.set_default_device("cpu") |
| torch.set_default_dtype(default_dtype) |
|
|
| if self.world_size > 1: |
| if rank == 0: |
| self.shm = SharedMemory(name="nanovllm", create=True, size=2**20) |
| dist.barrier() |
| else: |
| dist.barrier() |
| self.shm = SharedMemory(name="nanovllm") |
| self.loop() |
|
|
| def ensure_runtime_ready(self): |
| if self._runtime_ready: |
| |
| |
| |
| if self.enforce_eager: |
| return |
| current_sig = self._get_graph_capture_signature() |
| if current_sig == self._runtime_signature: |
| return |
| self._note_guard("runtime_reprepare_signature_change") |
| self.reset_runtime_state() |
| if self.model is None: |
| raise RuntimeError("LLM model object is not available.") |
| self._tie_word_embeddings_if_needed() |
| self.allocate_kv_cache() |
| self._prepare_model_sequence_state() |
| if not self.enforce_eager: |
| self.capture_cudagraph() |
| self._runtime_ready = True |
| self._runtime_signature = self._get_graph_capture_signature() |
|
|
| def reset_generation_state(self): |
| if self.model is None: |
| return |
| try: |
| for module in self.model.modules(): |
| reset_sequence_state = getattr(module, "reset_sequence_state", None) |
| if callable(reset_sequence_state): |
| reset_sequence_state() |
| continue |
| if hasattr(module, "conv_state"): |
| module.conv_state = None |
| if hasattr(module, "recurrent_state"): |
| module.recurrent_state = None |
| except Exception: |
| pass |
| self._logits_bias_cache.clear() |
| reset_context() |
|
|
| def _prepare_model_sequence_state(self): |
| if self.model is None: |
| return |
| model_device = self._get_model_device() |
| for module in self.model.modules(): |
| prepare = getattr(module, "prepare_sequence_state", None) |
| if callable(prepare): |
| prepare(self.config.max_num_seqs, model_device, self.dtype) |
|
|
| def _get_tied_embeddings(self): |
| if self.model is None: |
| return None |
| hf_config = getattr(self.config, "hf_config", None) |
| if not bool(getattr(hf_config, "tie_word_embeddings", False)): |
| return None |
| lm_head = getattr(self.model, "lm_head", None) |
| embed = getattr(self.model, "embed_tokens", None) |
| if lm_head is None or embed is None: |
| return None |
| lm_w = getattr(lm_head, "weight", None) |
| emb_w = getattr(embed, "weight", None) |
| if lm_w is None or emb_w is None: |
| return None |
| return lm_head, embed |
|
|
| def _tie_word_embeddings_if_needed(self): |
| tied = self._get_tied_embeddings() |
| if tied is None: |
| return |
| lm_head, embed = tied |
| if lm_head.weight is not embed.weight: |
| lm_head.register_parameter("weight", embed.weight) |
| if lm_head.weight is not embed.weight: |
| raise RuntimeError("Failed to retie lm_head.weight with embed_tokens.weight.") |
|
|
| def reset_runtime_state(self): |
| if not self._runtime_ready: |
| return |
| |
| try: |
| if self.model is not None: |
| for module in self.model.modules(): |
| if hasattr(module, "k_cache") and hasattr(module, "v_cache"): |
| module.k_cache = module.v_cache = torch.tensor([]) |
| release_sequence_state = getattr(module, "release_sequence_state", None) |
| if callable(release_sequence_state): |
| release_sequence_state() |
| continue |
| reset_sequence_state = getattr(module, "reset_sequence_state", None) |
| if callable(reset_sequence_state): |
| reset_sequence_state() |
| else: |
| if hasattr(module, "conv_state"): |
| module.conv_state = None |
| if hasattr(module, "recurrent_state"): |
| module.recurrent_state = None |
| except Exception: |
| pass |
| if hasattr(self, "kv_cache"): |
| try: |
| del self.kv_cache |
| except Exception: |
| pass |
| |
| |
| try: |
| self.clear_graph_cache() |
| except Exception: |
| pass |
| try: |
| for attr_name in ("graphs", "graph_vars", "graph_bs", "graph_pool"): |
| if hasattr(self, attr_name): |
| delattr(self, attr_name) |
| except Exception: |
| pass |
| try: |
| torch.cuda.synchronize() |
| except Exception: |
| pass |
| try: |
| torch.cuda.empty_cache() |
| except Exception: |
| pass |
| try: |
| torch.cuda.ipc_collect() |
| except Exception: |
| pass |
| self._logits_bias_cache.clear() |
| self._sampling_generator = None |
| self._runtime_signature = None |
| self._runtime_ready = False |
| gc.collect() |
|
|
| def _get_graph_capture_signature(self): |
| model_ptr = -1 |
| kv_ptr = -1 |
| try: |
| first_param = next(self.model.parameters()) |
| if first_param.is_cuda: |
| model_ptr = int(first_param.data_ptr()) |
| except Exception: |
| pass |
| try: |
| if hasattr(self, "kv_cache") and torch.is_tensor(self.kv_cache) and self.kv_cache.is_cuda: |
| kv_ptr = int(self.kv_cache.data_ptr()) |
| except Exception: |
| pass |
| return (model_ptr, kv_ptr, int(self.config.max_model_len), int(self.config.max_num_seqs)) |
|
|
| def _get_model_device(self) -> torch.device: |
| try: |
| return next(self.model.parameters()).device |
| except Exception: |
| return torch.device("cpu") |
|
|
| def _get_runtime_device(self) -> torch.device: |
| if torch.cuda.is_available(): |
| return torch.device("cuda", torch.cuda.current_device()) |
| if hasattr(torch.backends, "mps") and torch.backends.mps.is_available(): |
| return torch.device("mps") |
| return self._get_model_device() |
|
|
| def _pin_memory_enabled(self) -> bool: |
| return self._get_runtime_device().type == "cuda" |
|
|
| def _to_runtime_device(self, tensor: torch.Tensor) -> torch.Tensor: |
| device = self._get_runtime_device() |
| return tensor.to(device=device, non_blocking=device.type == "cuda") |
|
|
| def _drop_graph_cache_entry(self, cache_key): |
| entry = self._graph_cache.pop(cache_key, None) |
| if cache_key in self._graph_cache_order: |
| self._graph_cache_order.remove(cache_key) |
| if entry is None: |
| return |
| try: |
| del entry["graphs"] |
| del entry["pool"] |
| del entry["vars"] |
| del entry["bs"] |
| except Exception: |
| pass |
|
|
| def clear_graph_cache(self): |
| if self._graph_cache: |
| for key in list(self._graph_cache.keys()): |
| self._drop_graph_cache_entry(key) |
| self._graph_cache.clear() |
| self._graph_cache_order.clear() |
|
|
| def _note_guard(self, name: str, detail: str | None = None): |
| count = self._guard_counts.get(name, 0) + 1 |
| self._guard_counts[name] = count |
| if detail: |
| detail_key = (name, detail) |
| if detail_key not in self._guard_seen_details: |
| print(f"[nanovllm][guard] {name}: {detail}") |
| self._guard_seen_details.add(detail_key) |
| return |
| if count == 1: |
| print(f"[nanovllm][guard] {name}") |
|
|
| def _get_logits_bias(self, seq: Sequence, logits: torch.Tensor): |
| bias = getattr(seq, "logits_bias", None) |
| if bias is None or not torch.is_tensor(bias): |
| return None |
| key = (id(bias), logits.device, logits.dtype) |
| cached = self._logits_bias_cache.get(key) |
| if cached is not None: |
| return cached |
| cached = bias.to(device=logits.device, dtype=logits.dtype) |
| self._logits_bias_cache[key] = cached |
| return cached |
|
|
| def set_sampling_seed(self, seed: int | None): |
| if seed is None: |
| self._sampling_generator = None |
| return |
| device = self._get_runtime_device() |
| try: |
| generator = torch.Generator(device=device) |
| generator.manual_seed(int(seed)) |
| self._sampling_generator = generator |
| except Exception: |
| self._sampling_generator = None |
|
|
| @staticmethod |
| def _apply_logits_bias(logits_row: torch.Tensor, bias: torch.Tensor): |
| logits_row.add_(bias) |
|
|
| def _allocate_sample_buffers(self): |
| """Pre-allocate reusable buffers for sampling to avoid repeated tensor creation.""" |
| max_bs = self.config.max_num_seqs |
| max_tokens = self.config.max_num_batched_tokens |
| max_num_blocks = (self.config.max_model_len + self.block_size - 1) // self.block_size |
| pin_memory = self._pin_memory_enabled() |
| |
| |
| |
| self._cpu_temperatures = torch.zeros(max_bs, dtype=torch.float32, device="cpu", pin_memory=pin_memory) |
| self._cpu_cfg_scales = torch.zeros(max_bs, dtype=torch.float32, device="cpu", pin_memory=pin_memory) |
| self._cpu_top_ks = torch.zeros(max_bs, dtype=torch.int32, device="cpu", pin_memory=pin_memory) |
| self._cpu_top_ps = torch.zeros(max_bs, dtype=torch.float32, device="cpu", pin_memory=pin_memory) |
| self._cpu_min_ps = torch.zeros(max_bs, dtype=torch.float32, device="cpu", pin_memory=pin_memory) |
| self._cpu_repetition_penalties = torch.zeros(max_bs, dtype=torch.float32, device="cpu", pin_memory=pin_memory) |
| |
| |
| self._cpu_input_ids = torch.zeros(max_bs, dtype=torch.int64, device="cpu", pin_memory=pin_memory) |
| self._cpu_positions = torch.zeros(max_bs, dtype=torch.int64, device="cpu", pin_memory=pin_memory) |
| self._cpu_slot_mapping = torch.zeros(max_bs, dtype=torch.int32, device="cpu", pin_memory=pin_memory) |
| self._cpu_context_lens = torch.zeros(max_bs, dtype=torch.int32, device="cpu", pin_memory=pin_memory) |
| |
| |
| self._cpu_prefill_input_ids = torch.zeros(max_tokens, dtype=torch.int64, device="cpu", pin_memory=pin_memory) |
| self._cpu_prefill_positions = torch.zeros(max_tokens, dtype=torch.int64, device="cpu", pin_memory=pin_memory) |
| self._cpu_prefill_cu_seqlens = torch.zeros(max_bs + 1, dtype=torch.int32, device="cpu", pin_memory=pin_memory) |
| self._cpu_prefill_slot_mapping = torch.zeros(max_tokens, dtype=torch.int32, device="cpu", pin_memory=pin_memory) |
| |
| |
| self._cpu_block_tables = torch.zeros(max_bs, max_num_blocks, dtype=torch.int32, device="cpu", pin_memory=pin_memory) |
| |
| |
| |
| self._seq_token_ids_buffer = torch.zeros(max_bs, self.config.max_model_len, dtype=torch.int64, device="cpu", pin_memory=pin_memory) |
|
|
| def _release_sample_buffers(self): |
| buffer_names = [ |
| "_cpu_temperatures", |
| "_cpu_cfg_scales", |
| "_cpu_top_ks", |
| "_cpu_top_ps", |
| "_cpu_min_ps", |
| "_cpu_repetition_penalties", |
| "_cpu_input_ids", |
| "_cpu_positions", |
| "_cpu_slot_mapping", |
| "_cpu_context_lens", |
| "_cpu_prefill_input_ids", |
| "_cpu_prefill_positions", |
| "_cpu_prefill_cu_seqlens", |
| "_cpu_prefill_slot_mapping", |
| "_cpu_block_tables", |
| "_seq_token_ids_buffer", |
| ] |
| for name in buffer_names: |
| if hasattr(self, name): |
| try: |
| delattr(self, name) |
| except Exception: |
| pass |
|
|
| def exit(self): |
| try: |
| self.reset_runtime_state() |
| except Exception: |
| pass |
| self._release_sample_buffers() |
| self._logits_bias_cache.clear() |
| self._guard_counts.clear() |
| self._guard_seen_details.clear() |
| if hasattr(self, "sampler"): |
| self.sampler = None |
| if hasattr(self, "model"): |
| self.model = None |
| if self.world_size > 1: |
| self.shm.close() |
| dist.barrier() |
| if self.rank == 0: |
| self.shm.unlink() |
| if not self.enforce_eager: |
| if hasattr(self, "graphs"): |
| del self.graphs |
| if hasattr(self, "graph_vars"): |
| del self.graph_vars |
| if hasattr(self, "graph_bs"): |
| del self.graph_bs |
| if hasattr(self, "graph_pool"): |
| del self.graph_pool |
| try: |
| torch.cuda.synchronize() |
| except Exception: |
| pass |
| try: |
| torch.cuda.empty_cache() |
| except Exception: |
| pass |
| try: |
| torch.cuda.ipc_collect() |
| except Exception: |
| pass |
| if dist.is_initialized(): |
| dist.destroy_process_group() |
|
|
| def loop(self): |
| while True: |
| method_name, args = self.read_shm() |
| self.call(method_name, *args) |
| if method_name == "exit": |
| break |
|
|
| def read_shm(self): |
| assert self.world_size > 1 and self.rank > 0 |
| self.event.wait() |
| n = int.from_bytes(self.shm.buf[0:4], "little") |
| method_name, *args = pickle.loads(self.shm.buf[4:n+4]) |
| self.event.clear() |
| return method_name, args |
|
|
| def write_shm(self, method_name, *args): |
| assert self.world_size > 1 and self.rank == 0 |
| data = pickle.dumps([method_name, *args]) |
| n = len(data) |
| self.shm.buf[0:4] = n.to_bytes(4, "little") |
| self.shm.buf[4:n+4] = data |
| for event in self.event: |
| event.set() |
|
|
| def call(self, method_name, *args): |
| if self.world_size > 1 and self.rank == 0: |
| self.write_shm(method_name, *args) |
| method = getattr(self, method_name, None) |
| return method(*args) |
|
|
| def _get_kv_cache_modules(self): |
| if self.model is None: |
| return [] |
| return [module for module in self.model.modules() if hasattr(module, "k_cache") and hasattr(module, "v_cache")] |
|
|
| def allocate_kv_cache(self): |
| config = self.config |
| hf_config = config.hf_config |
| runtime_device = self._get_runtime_device() |
| is_cuda_runtime = runtime_device.type == "cuda" |
| num_kv_heads = hf_config.num_key_value_heads // self.world_size |
| head_dim = getattr(hf_config, "head_dim", hf_config.hidden_size // hf_config.num_attention_heads) |
| kv_cache_modules = self._get_kv_cache_modules() |
| kv_cache_layer_count = len(kv_cache_modules) |
| block_bytes = 2 * kv_cache_layer_count * self.block_size * num_kv_heads * head_dim * self.dtype.itemsize |
|
|
| |
| required_blocks_per_seq = (config.max_model_len + self.block_size - 1) // self.block_size |
| required_total_blocks = required_blocks_per_seq * max(1, config.max_num_seqs) |
| config.num_kvcache_blocks = max(1, int(required_total_blocks)) |
| required_kv_bytes = config.num_kvcache_blocks * block_bytes |
| if is_cuda_runtime: |
| free, total = torch.cuda.mem_get_info() |
| current = torch.cuda.memory_stats()["allocated_bytes.all.current"] |
| target_total_usage = total * config.gpu_memory_utilization |
| allowed_kv_bytes = max(0, target_total_usage - current) |
| if required_kv_bytes > allowed_kv_bytes: |
| raise RuntimeError( |
| f"Insufficient GPU memory for strict KV cache sizing under gpu_memory_utilization={config.gpu_memory_utilization:.2f}. " |
| f"Required KV: {required_kv_bytes / 1024**3:.2f} GB " |
| f"(blocks={config.num_kvcache_blocks}, block={block_bytes / 1024**2:.2f} MB), " |
| f"Allowed by limit: {allowed_kv_bytes / 1024**3:.2f} GB, " |
| f"Free: {free / 1024**3:.2f} GB, Current: {current / 1024**3:.2f} GB, " |
| f"Requested max_model_len={config.max_model_len}, max_num_seqs={config.max_num_seqs}." |
| ) |
| try: |
| self.kv_cache = torch.empty( |
| 2, |
| kv_cache_layer_count, |
| config.num_kvcache_blocks, |
| self.block_size, |
| num_kv_heads, |
| head_dim, |
| device=runtime_device, |
| dtype=self.dtype, |
| ) |
| except RuntimeError as exc: |
| if "out of memory" in str(exc).lower(): |
| extra = "" |
| if is_cuda_runtime: |
| free_now, total_now = torch.cuda.mem_get_info() |
| extra = f" Current free VRAM: {free_now / 1024**3:.2f} GB / {total_now / 1024**3:.2f} GB total." |
| raise RuntimeError( |
| f"Failed to allocate strict KV cache ({required_kv_bytes / 1024**3:.2f} GB) for " |
| f"max_model_len={config.max_model_len}, max_num_seqs={config.max_num_seqs} on {runtime_device}.{extra}" |
| ) from exc |
| raise |
| for layer_id, module in enumerate(kv_cache_modules): |
| module.k_cache = self.kv_cache[0, layer_id] |
| module.v_cache = self.kv_cache[1, layer_id] |
|
|
| def prepare_block_tables(self, seqs: list[Sequence]): |
| bs = len(seqs) |
| max_len = max(len(seq.block_table) for seq in seqs) |
| block_tables = self._cpu_block_tables[:bs, :max_len] |
| block_tables.fill_(-1) |
| for row, seq in enumerate(seqs): |
| if not seq.block_table: |
| continue |
| block_tables[row, :len(seq.block_table)] = torch.tensor(seq.block_table, dtype=torch.int32, device="cpu") |
| return self._to_runtime_device(block_tables) |
|
|
| def prepare_prefill(self, seqs: list[Sequence]): |
| use_prompt_embeds = any(getattr(seq, "prompt_embeds", None) is not None for seq in seqs) |
| if use_prompt_embeds and not all(getattr(seq, "prompt_embeds", None) is not None for seq in seqs): |
| raise RuntimeError("Mixed embedded/non-embedded prefill batches are not supported.") |
| input_ids = [] |
| positions = [] |
| prompt_embeds = [] |
| prompt_position_ids = [] |
| cu_seqlens_q = [0] |
| cu_seqlens_k = [0] |
| max_seqlen_q = 0 |
| max_seqlen_k = 0 |
| slot_mapping = [] |
| block_tables = None |
| has_previous_state = any(int(getattr(seq, "num_cached_tokens", 0) or 0) > 0 for seq in seqs) |
| for seq in seqs: |
| seqlen = len(seq) |
| input_ids.extend(seq[seq.num_cached_tokens:]) |
| seqlen_q = seqlen - seq.num_cached_tokens |
| seqlen_k = seqlen |
| if use_prompt_embeds: |
| seq_prompt_embeds = getattr(seq, "prompt_embeds", None) |
| seq_position_ids = getattr(seq, "prompt_position_ids", None) |
| if seq_prompt_embeds is None or seq_position_ids is None: |
| raise RuntimeError("Embedded prefill requires both prompt_embeds and prompt_position_ids.") |
| seq_prompt_embeds = seq_prompt_embeds[seq.num_cached_tokens:seqlen] |
| if seq_prompt_embeds.ndim != 2 or seq_prompt_embeds.shape[0] != seqlen_q: |
| raise RuntimeError("Embedded prefill prompt_embeds shape does not match uncached prompt length.") |
| if seq_position_ids.ndim == 3: |
| seq_position_ids = seq_position_ids[:, 0] |
| if seq_position_ids.ndim != 2 or seq_position_ids.shape[0] != 3: |
| raise RuntimeError("Embedded prefill prompt_position_ids must have shape [3, seq_len].") |
| seq_position_ids = seq_position_ids[:, seq.num_cached_tokens:seqlen] |
| if seq_position_ids.shape[1] != seqlen_q: |
| raise RuntimeError("Embedded prefill prompt_position_ids shape does not match uncached prompt length.") |
| prompt_embeds.append(seq_prompt_embeds) |
| prompt_position_ids.append(seq_position_ids) |
| else: |
| positions.extend(list(range(seq.num_cached_tokens, seqlen))) |
| cu_seqlens_q.append(cu_seqlens_q[-1] + seqlen_q) |
| cu_seqlens_k.append(cu_seqlens_k[-1] + seqlen_k) |
| max_seqlen_q = max(seqlen_q, max_seqlen_q) |
| max_seqlen_k = max(seqlen_k, max_seqlen_k) |
| if not seq.block_table: |
| slot_mapping.extend([-1] * seqlen_q) |
| continue |
| cached_tokens = max(0, int(seq.num_cached_tokens or 0)) |
| cached_partial_tokens = cached_tokens % self.block_size |
| for i in range(seq.num_cached_blocks, seq.num_blocks): |
| start = seq.block_table[i] * self.block_size |
| if i == seq.num_cached_blocks and cached_partial_tokens > 0: |
| start += cached_partial_tokens |
| if i != seq.num_blocks - 1: |
| end = seq.block_table[i] * self.block_size + self.block_size |
| else: |
| end = seq.block_table[i] * self.block_size + seq.last_block_num_tokens |
| if end > start: |
| slot_mapping.extend(list(range(start, end))) |
| if cu_seqlens_k[-1] > cu_seqlens_q[-1]: |
| block_tables = self.prepare_block_tables(seqs) |
| if use_prompt_embeds: |
| input_ids = None |
| positions = self._to_runtime_device(torch.cat(prompt_position_ids, dim=1).unsqueeze(1).contiguous()) |
| inputs_embeds = self._to_runtime_device(torch.cat(prompt_embeds, dim=0).unsqueeze(0).contiguous()) |
| else: |
| pin_memory = self._pin_memory_enabled() |
| input_ids = self._to_runtime_device(torch.tensor(input_ids, dtype=torch.int64, pin_memory=pin_memory)) |
| positions = self._to_runtime_device(torch.tensor(positions, dtype=torch.int64, pin_memory=pin_memory)) |
| inputs_embeds = None |
| pin_memory = self._pin_memory_enabled() |
| cu_seqlens_q = self._to_runtime_device(torch.tensor(cu_seqlens_q, dtype=torch.int32, pin_memory=pin_memory)) |
| cu_seqlens_k = self._to_runtime_device(torch.tensor(cu_seqlens_k, dtype=torch.int32, pin_memory=pin_memory)) |
| slot_mapping = self._to_runtime_device(torch.tensor(slot_mapping, dtype=torch.int32, pin_memory=pin_memory)) |
| set_context(True, cu_seqlens_q, cu_seqlens_k, max_seqlen_q, max_seqlen_k, slot_mapping, None, block_tables, has_previous_state=has_previous_state) |
| return input_ids, positions, inputs_embeds |
|
|
| @torch.inference_mode() |
| def prefill_only(self, seqs: list[Sequence]) -> None: |
| self.ensure_runtime_ready() |
| input_ids, positions, inputs_embeds = self.prepare_prefill(seqs) |
| self.run_model(input_ids, positions, True, inputs_embeds=inputs_embeds) |
| for seq in seqs: |
| seq.clear_prompt_data() |
| reset_context() |
|
|
| def prepare_decode(self, seqs: list[Sequence]): |
| """Optimized decode preparation using pre-allocated buffers.""" |
| bs = len(seqs) |
| |
| |
| for i, seq in enumerate(seqs): |
| self._cpu_input_ids[i] = seq.last_token |
| self._cpu_positions[i] = len(seq) - 1 + int(getattr(seq, "position_offset", 0) or 0) |
| self._cpu_context_lens[i] = len(seq) |
| self._cpu_slot_mapping[i] = seq.block_table[-1] * self.block_size + seq.last_block_num_tokens - 1 |
| |
| |
| input_ids = self._to_runtime_device(self._cpu_input_ids[:bs]) |
| positions = self._to_runtime_device(self._cpu_positions[:bs]) |
| slot_mapping = self._to_runtime_device(self._cpu_slot_mapping[:bs]) |
| context_lens = self._to_runtime_device(self._cpu_context_lens[:bs]) |
| block_tables = self.prepare_block_tables(seqs) |
| set_context(False, slot_mapping=slot_mapping, context_lens=context_lens, block_tables=block_tables) |
| return input_ids, positions |
|
|
| def prepare_sample(self, seqs: list[Sequence], is_cfg_batch: bool = False): |
| """Optimized sample preparation using pre-allocated buffers.""" |
| if is_cfg_batch: |
| num_seqs = len(seqs) // 2 |
| target_seqs = seqs[:num_seqs] |
| else: |
| num_seqs = len(seqs) |
| target_seqs = seqs |
| |
| |
| top_ks_is_zero = True |
| top_ps_is_one = True |
| min_ps_is_zero = True |
| repetition_penalties_is_one = True |
| for i, seq in enumerate(target_seqs): |
| self._cpu_temperatures[i] = seq.temperature |
| self._cpu_cfg_scales[i] = seq.cfg_scale |
| self._cpu_top_ks[i] = seq.top_k if seq.top_k is not None else 0 |
| if seq.top_k is not None and seq.top_k > 0: |
| top_ks_is_zero = False |
| self._cpu_top_ps[i] = seq.top_p if seq.top_p is not None else 1.0 |
| if seq.top_p is not None and seq.top_p != 1.0: |
| top_ps_is_one = False |
| self._cpu_min_ps[i] = seq.min_p if seq.min_p is not None else 0.0 |
| if seq.min_p is not None and seq.min_p > 0.0: |
| min_ps_is_zero = False |
| self._cpu_repetition_penalties[i] = seq.repetition_penalty if seq.repetition_penalty is not None else 1.0 |
| if seq.repetition_penalty is not None and seq.repetition_penalty != 1.0: |
| repetition_penalties_is_one = False |
| |
| |
| temperatures = self._to_runtime_device(self._cpu_temperatures[:num_seqs]) |
| cfg_scales = self._to_runtime_device(self._cpu_cfg_scales[:num_seqs]) |
| top_ks = self._to_runtime_device(self._cpu_top_ks[:num_seqs]) if not top_ks_is_zero else None |
| top_ps = self._to_runtime_device(self._cpu_top_ps[:num_seqs]) if not top_ps_is_one else None |
| min_ps = self._to_runtime_device(self._cpu_min_ps[:num_seqs]) if not min_ps_is_zero else None |
| repetition_penalties = self._to_runtime_device(self._cpu_repetition_penalties[:num_seqs]) if not repetition_penalties_is_one else None |
| |
| return temperatures, cfg_scales, top_ks, top_ps, min_ps, repetition_penalties |
|
|
| @torch.inference_mode() |
| def run_model(self, input_ids: torch.Tensor | None, positions: torch.Tensor, is_prefill: bool, inputs_embeds: torch.Tensor | None = None): |
| decode_batch_size = input_ids.size(0) if input_ids is not None else int(inputs_embeds.shape[0]) |
| model_kwargs = {"input_ids": input_ids, "positions": positions} |
| if inputs_embeds is not None: |
| model_kwargs["inputs_embeds"] = inputs_embeds |
| if is_prefill or self.enforce_eager or decode_batch_size > 512: |
| return self.model.compute_logits(self.model(**model_kwargs)) |
| else: |
| bs = decode_batch_size |
| context = get_context() |
| |
| |
| |
| |
| max_num_blocks = self.graph_vars["block_tables"].size(1) |
| if context.block_tables.size(1) > max_num_blocks: |
| |
| self._note_guard( |
| "cudagraph_fallback_block_table_cols", |
| f"requested={context.block_tables.size(1)} max={max_num_blocks}", |
| ) |
| return self.model.compute_logits(self.model(**model_kwargs)) |
| |
| |
| |
| if context.block_tables.size(0) != bs: |
| |
| self._note_guard( |
| "cudagraph_fallback_block_table_rows", |
| f"rows={context.block_tables.size(0)} bs={bs}", |
| ) |
| return self.model.compute_logits(self.model(**model_kwargs)) |
| |
| |
| if context.slot_mapping.size(0) != bs or context.context_lens.size(0) != bs: |
| |
| self._note_guard( |
| "cudagraph_fallback_context_shape", |
| f"slot={context.slot_mapping.size(0)} ctx={context.context_lens.size(0)} bs={bs}", |
| ) |
| return self.model.compute_logits(self.model(**model_kwargs)) |
| |
| graph = self.graphs[next(x for x in self.graph_bs if x >= bs)] |
| graph_vars = self.graph_vars |
| graph_vars["input_ids"][:bs] = input_ids |
| graph_vars["positions"][:bs] = positions |
| graph_vars["slot_mapping"].fill_(-1) |
| graph_vars["slot_mapping"][:bs] = context.slot_mapping |
| graph_vars["context_lens"].zero_() |
| graph_vars["context_lens"][:bs] = context.context_lens |
| |
| graph_vars["block_tables"][:bs].fill_(-1) |
| graph_vars["block_tables"][:bs, :context.block_tables.size(1)] = context.block_tables |
| graph.replay() |
| return self.model.compute_logits(graph_vars["outputs"][:bs]) |
|
|
| def run(self, seqs: list[Sequence], is_prefill: bool) -> list[int]: |
| """Run model forward and sampling. For CFG sequences, batch is structured as: |
| [cond_seq1, cond_seq2, ..., uncond_seq1, uncond_seq2, ...] |
| where uncond_seqi is the paired unconditional sequence of cond_seqi.""" |
| self.ensure_runtime_ready() |
| |
| is_cfg_batch = seqs[0].cfg_scale > 1.0 and seqs[0].paired_seq is not None |
| if is_cfg_batch: |
| |
| num_cond = len(seqs) // 2 |
| cond_seqs = seqs[:num_cond] |
| |
| |
| |
| if is_prefill: |
| input_ids, positions, inputs_embeds = self.prepare_prefill(seqs) |
| else: |
| input_ids, positions = self.prepare_decode(seqs) |
| inputs_embeds = None |
| sample_params = self.prepare_sample(seqs, is_cfg_batch=True) if self.rank == 0 else None |
| if sample_params is not None: |
| temperatures, cfg_scales, top_ks, top_ps, min_ps, repetition_penalties = sample_params |
| else: |
| temperatures = cfg_scales = top_ks = top_ps = min_ps = repetition_penalties = None |
| |
| |
| logits_all = self.run_model(input_ids, positions, is_prefill, inputs_embeds=inputs_embeds) |
| if is_prefill: |
| for seq in seqs: |
| seq.clear_prompt_data() |
| reset_context() |
| |
| if self.rank == 0: |
| |
| logits_cond = logits_all[:num_cond] |
| logits_uncond = logits_all[num_cond:] |
| |
| |
| if repetition_penalties is not None: |
| for i, seq in enumerate(cond_seqs): |
| penalty = repetition_penalties[i].item() |
| if penalty != 1.0: |
| |
| completion_tokens = torch.tensor(seq.completion_token_ids, device=logits_cond.device) |
| if len(completion_tokens) > 0: |
| |
| token_mask = torch.zeros(logits_cond.shape[1], dtype=torch.bool, device=logits_cond.device) |
| token_mask[completion_tokens] = True |
| |
| |
| |
| penalty_scores = torch.where( |
| logits_cond[i] < 0, |
| logits_cond[i] * penalty, |
| logits_cond[i] / penalty |
| ) |
| |
| logits_cond[i] = torch.where(token_mask, penalty_scores, logits_cond[i]) |
| |
| |
| cfg_scales_tensor = cfg_scales.unsqueeze(1) |
| logits_cfg = logits_uncond + cfg_scales_tensor * (logits_cond - logits_uncond) |
|
|
| |
| for i, seq in enumerate(cond_seqs): |
| bias = self._get_logits_bias(seq, logits_cfg) |
| if bias is not None: |
| self._apply_logits_bias(logits_cfg[i], bias) |
| |
| |
| for i, seq in enumerate(cond_seqs): |
| if seq.logits_processor is not None: |
| |
| seq_input_ids = torch.tensor([seq.token_ids], device=logits_cfg.device) |
| |
| logits_cfg[i:i+1] = seq.logits_processor(seq_input_ids, logits_cfg[i:i+1]) |
|
|
| |
| |
| |
| |
| token_ids_cfg = self.sampler( |
| logits_cfg, |
| temperatures, |
| top_ks=top_ks if top_ks is not None else None, |
| top_ps=top_ps if top_ps is not None else None, |
| min_ps=min_ps if min_ps is not None else None, |
| repetition_penalties=None, |
| generator=self._sampling_generator, |
| |
| ).tolist() |
| |
| |
| |
| |
| if cond_seqs and cond_seqs[0].logits_processor_update_state is not None: |
| cond_seqs[0].logits_processor_update_state(token_ids_cfg[0]) |
| |
| |
| return token_ids_cfg |
| else: |
| return None |
| else: |
| |
| if is_prefill: |
| input_ids, positions, inputs_embeds = self.prepare_prefill(seqs) |
| else: |
| input_ids, positions = self.prepare_decode(seqs) |
| inputs_embeds = None |
| sample_params = self.prepare_sample(seqs, is_cfg_batch=False) if self.rank == 0 else None |
| if sample_params is not None: |
| temperatures, cfg_scales, top_ks, top_ps, min_ps, repetition_penalties = sample_params |
| else: |
| temperatures = cfg_scales = top_ks = top_ps = min_ps = repetition_penalties = None |
| logits = self.run_model(input_ids, positions, is_prefill, inputs_embeds=inputs_embeds) |
| if is_prefill: |
| for seq in seqs: |
| seq.clear_prompt_data() |
| reset_context() |
| |
| if self.rank == 0: |
| |
| if repetition_penalties is not None: |
| for i, seq in enumerate(seqs): |
| penalty = repetition_penalties[i].item() |
| if penalty != 1.0: |
| |
| completion_tokens = torch.tensor(seq.completion_token_ids, device=logits.device) |
| if len(completion_tokens) > 0: |
| |
| token_mask = torch.zeros(logits.shape[1], dtype=torch.bool, device=logits.device) |
| token_mask[completion_tokens] = True |
| |
| |
| |
| penalty_scores = torch.where( |
| logits[i] < 0, |
| logits[i] * penalty, |
| logits[i] / penalty |
| ) |
| |
| logits[i] = torch.where(token_mask, penalty_scores, logits[i]) |
| |
| |
| |
| logits = logits.clone() |
| for i, seq in enumerate(seqs): |
| bias = self._get_logits_bias(seq, logits) |
| if bias is not None: |
| self._apply_logits_bias(logits[i], bias) |
| for i, seq in enumerate(seqs): |
| if seq.logits_processor is not None: |
| |
| seq_input_ids = torch.tensor([seq.token_ids], device=logits.device) |
| |
| processed = seq.logits_processor(seq_input_ids, logits[i:i+1].clone()) |
| logits[i] = processed[0] |
|
|
| |
| |
| |
| token_ids = self.sampler( |
| logits, |
| temperatures, |
| top_ks=top_ks if top_ks is not None else None, |
| top_ps=top_ps if top_ps is not None else None, |
| min_ps=min_ps if min_ps is not None else None, |
| repetition_penalties=None, |
| generator=self._sampling_generator, |
| |
| ).tolist() |
| |
| |
| |
| |
| |
| if seqs and seqs[0].logits_processor_update_state is not None: |
| seqs[0].logits_processor_update_state(token_ids[0]) |
| |
| return token_ids |
| else: |
| return None |
|
|
| @torch.inference_mode() |
| def capture_cudagraph(self): |
| if self._get_runtime_device().type != "cuda": |
| self.enforce_eager = True |
| return |
| config = self.config |
| cache_key = (config.max_model_len, config.max_num_seqs) |
| model_device = torch.device("cuda") if torch.cuda.is_available() else self._get_model_device() |
| cached = self._graph_cache.get(cache_key) |
| if cached is not None: |
| current_sig = self._get_graph_capture_signature() |
| if cached.get("sig") == current_sig: |
| self.graphs = cached["graphs"] |
| self.graph_pool = cached["pool"] |
| self.graph_vars = cached["vars"] |
| self.graph_bs = cached["bs"] |
| if cache_key in self._graph_cache_order: |
| self._graph_cache_order.remove(cache_key) |
| self._graph_cache_order.append(cache_key) |
| return |
| self._drop_graph_cache_entry(cache_key) |
| hf_config = config.hf_config |
| max_bs = min(self.config.max_num_seqs, 512) |
| max_num_blocks = (config.max_model_len + self.block_size - 1) // self.block_size |
| input_ids = torch.zeros(max_bs, dtype=torch.int64, device=model_device) |
| positions = torch.zeros(max_bs, dtype=torch.int64, device=model_device) |
| slot_mapping = torch.zeros(max_bs, dtype=torch.int32, device=model_device) |
| context_lens = torch.zeros(max_bs, dtype=torch.int32, device=model_device) |
| block_tables = torch.zeros(max_bs, max_num_blocks, dtype=torch.int32, device=model_device) |
| outputs = torch.zeros(max_bs, hf_config.hidden_size, device=model_device, dtype=self.dtype) |
| base_graph_bs = [1, 2, 4, 8] |
| self.graph_bs = [bs for bs in base_graph_bs if bs <= max_bs] |
| if max_bs > 8: |
| self.graph_bs.extend(range(16, max_bs + 1, 16)) |
| if not self.graph_bs: |
| self.graph_bs = [max_bs] |
| self.graphs = {} |
| self.graph_pool = self._graph_pool_seed |
|
|
| for bs in reversed(self.graph_bs): |
| graph = torch.cuda.CUDAGraph() |
| set_context(False, slot_mapping=slot_mapping[:bs], context_lens=context_lens[:bs], block_tables=block_tables[:bs]) |
| outputs[:bs] = self.model(input_ids[:bs], positions[:bs]) |
| with torch.cuda.graph(graph, self.graph_pool): |
| outputs[:bs] = self.model(input_ids[:bs], positions[:bs]) |
| if self.graph_pool is None: |
| self.graph_pool = graph.pool() |
| self.graphs[bs] = graph |
| torch.cuda.synchronize() |
| reset_context() |
|
|
| self.graph_vars = dict( |
| input_ids=input_ids, |
| positions=positions, |
| slot_mapping=slot_mapping, |
| context_lens=context_lens, |
| block_tables=block_tables, |
| outputs=outputs, |
| ) |
| self._graph_cache[cache_key] = { |
| "graphs": self.graphs, |
| "pool": self.graph_pool, |
| "vars": self.graph_vars, |
| "bs": self.graph_bs, |
| "sig": self._get_graph_capture_signature(), |
| } |
| if cache_key in self._graph_cache_order: |
| self._graph_cache_order.remove(cache_key) |
| self._graph_cache_order.append(cache_key) |
| while len(self._graph_cache_order) > 5: |
| old_key = self._graph_cache_order.pop(0) |
| self._drop_graph_cache_entry(old_key) |
|
|