from __future__ import annotations from typing import TYPE_CHECKING, Literal, NamedTuple, Optional, Union import torch from sglang.jit_kernel.utils import ( cache_once, is_arch_support_pdl, load_jit, make_cpp_args, ) from sglang.srt.environ import envs from .utils import make_name if TYPE_CHECKING: from tvm_ffi.module import Module @cache_once def _jit_common_module() -> Module: return load_jit( make_name("common"), cuda_files=["deepseek_v4/common.cuh"], cuda_wrappers=[("plan_compress_prefill", "plan_compress_prefill")], ) @cache_once def _jit_compress_128_online_plan_module() -> Module: """Host-side plan generator for online compress 128 (no template args).""" return load_jit( make_name("compress_128_online_plan"), cuda_files=["deepseek_v4/c128_online.cuh"], cuda_wrappers=[ ("plan_compress_online_prefill", "plan_compress_online_prefill"), ], ) @cache_once def _jit_compress_128_online_module(head_dim: int) -> Module: """Online compress 128 kernel: ring_size=1, per-index (max, sum, kv) state.""" args = make_cpp_args(head_dim, is_arch_support_pdl()) kernel_class = f"FlashCompress128OnlineKernel<{args}>" return load_jit( make_name("compress_128_online"), *args, cuda_files=["deepseek_v4/c128_online.cuh"], cuda_wrappers=[ ("decode", f"{kernel_class}::run_decode"), ("prefill", f"{kernel_class}::run_prefill"), ], extra_cuda_cflags=["-use_fast_math"], ) @cache_once def _jit_norm_rope_module( dtype: torch.dtype, head_dim: int, rope_dim: int, ) -> Module: args = make_cpp_args(dtype, head_dim, rope_dim, is_arch_support_pdl()) return load_jit( make_name("fused_norm_rope"), *args, cuda_files=["deepseek_v4/fused_norm_rope.cuh"], cuda_wrappers=[ ("forward", f"FusedNormRopeKernel<{args}>::forward"), ], ) @cache_once def _jit_compress_module( head_dim: int, dtype_in: torch.dtype, dtype_out: torch.dtype, ratio: Literal[4, 128], ) -> Module: args = make_cpp_args(head_dim, dtype_in, dtype_out, is_arch_support_pdl()) kernel_class = f"FlashCompress{ratio}Kernel<{args}>" return load_jit( make_name(f"compress_{ratio}"), *args, cuda_files=[f"deepseek_v4/c{ratio}.cuh"], cuda_wrappers=[ ("decode", f"{kernel_class}::run_decode"), ("prefill", f"{kernel_class}::run_prefill"), ], extra_cuda_cflags=["-use_fast_math"], ) class CompressorPrefillPlan(NamedTuple): compress_ratio: int compress_plan: torch.Tensor write_plan: torch.Tensor def copy_(self, other: CompressorPrefillPlan) -> None: assert self.compress_ratio == other.compress_ratio self.compress_plan.copy_(other.compress_plan) self.write_plan.copy_(other.write_plan) @staticmethod def generate( compress_ratio: Literal[4, 128], num_q_tokens: int, seq_lens: torch.Tensor, extend_lens: torch.Tensor, device: torch.device, use_cuda_graph: bool = False, ) -> CompressorPrefillPlan: from sglang.srt.environ import envs # Online c128 keeps the same NamedTuple shape (compress_plan, write_plan) # so call sites that splat `*plan[1:]` continue to work, but the C++ # plan struct semantics differ (last-token coords + window_len). if compress_ratio == 128 and envs.SGLANG_OPT_USE_ONLINE_COMPRESS.get(): return CompressorPrefillPlan._generate_online( num_q_tokens=num_q_tokens, seq_lens=seq_lens, extend_lens=extend_lens, device=device, use_cuda_graph=use_cuda_graph, ) assert seq_lens.device == extend_lens.device seq_lens = seq_lens.to(torch.int64) extend_lens = extend_lens.to(torch.int64) plan_tensor = torch.empty( (2, num_q_tokens, 16), dtype=torch.uint8, device=seq_lens.device, pin_memory=seq_lens.is_cpu, ) module = _jit_common_module() is_overlap = compress_ratio == 4 plan_lens = module.plan_compress_prefill( extend_lens, seq_lens, plan_tensor[0], plan_tensor[1], compress_ratio, is_overlap, use_cuda_graph, ) return CompressorPrefillPlan( compress_ratio, plan_tensor[0, : plan_lens[0]].to(device, non_blocking=True), plan_tensor[1, : plan_lens[1]].to(device, non_blocking=True), ) @staticmethod def _generate_online( num_q_tokens: int, seq_lens: torch.Tensor, extend_lens: torch.Tensor, device: torch.device, use_cuda_graph: bool, ) -> CompressorPrefillPlan: # Online plan host-side path: only CPU/cuda-host implemented today. # Move inputs to CPU pinned memory then bounce the result to device. seq_lens_cpu = seq_lens.detach().to(torch.int64).cpu() extend_lens_cpu = extend_lens.detach().to(torch.int64).cpu() plan_tensor = torch.empty( (2, num_q_tokens, 16), dtype=torch.uint8, device="cpu", pin_memory=True, ) module = _jit_compress_128_online_plan_module() plan_lens = module.plan_compress_online_prefill( extend_lens_cpu, seq_lens_cpu, plan_tensor[0], plan_tensor[1], use_cuda_graph, ) return CompressorPrefillPlan( 128, plan_tensor[0, : plan_lens[0]].to(device, non_blocking=True), plan_tensor[1, : plan_lens[1]].to(device, non_blocking=True), ) @property def is_decode(self) -> bool: return False class CompressorDecodePlan(NamedTuple): compress_ratio: int seq_lens: torch.Tensor def copy_(self, other: CompressorDecodePlan) -> None: assert self.compress_ratio == other.compress_ratio self.seq_lens.copy_(other.seq_lens) @property def is_decode(self) -> bool: return True def compress_plan( compress_ratio: Literal[4, 128], num_q_tokens: int, seq_lens: torch.Tensor, extend_lens: Optional[torch.Tensor], device: torch.device, ) -> Union[CompressorDecodePlan, CompressorPrefillPlan]: if extend_lens is not None: return CompressorPrefillPlan.generate( compress_ratio, num_q_tokens, seq_lens, extend_lens, device, ) else: assert num_q_tokens == len(seq_lens) seq_lens = seq_lens.to(device, non_blocking=True) return CompressorDecodePlan(compress_ratio, seq_lens) def compress_forward( kv_score_buffer: torch.Tensor, kv_score_input: torch.Tensor, ape: torch.Tensor, indices: torch.Tensor, plan: Union[CompressorDecodePlan, CompressorPrefillPlan, None] = None, extra_data: Optional[torch.Tensor] = None, *, head_dim: int, compress_ratio: Literal[4, 128], out: Optional[torch.Tensor] = None, seq_lens: Optional[torch.Tensor] = None, extend_lens: Optional[torch.Tensor] = None, ) -> torch.Tensor: assert head_dim % 128 == 0 num_q_tokens = kv_score_input.shape[0] if out is None: out = kv_score_input.new_empty((num_q_tokens, head_dim)) if plan is None: assert seq_lens is not None plan = compress_plan( compress_ratio, num_q_tokens, seq_lens, extend_lens, kv_score_input.device, ) assert plan.compress_ratio == compress_ratio, "Mismatched compress ratio in plan!" # Online c128: separate JIT module, fp32 state, no compile-time dtypes. if compress_ratio == 128 and envs.SGLANG_OPT_USE_ONLINE_COMPRESS.get(): online_module = _jit_compress_128_online_module(head_dim=head_dim) F = online_module.decode if plan.is_decode else online_module.prefill F(kv_score_buffer, kv_score_input, out, ape, indices, *plan[1:], extra_data) return out module = _jit_compress_module( head_dim, kv_score_input.dtype, out.dtype, compress_ratio, ) F = module.decode if plan.is_decode else module.prefill F(kv_score_buffer, kv_score_input, out, ape, indices, *plan[1:], extra_data) return out def compress_fused_norm_rope_inplace( kv: torch.Tensor, weight: torch.Tensor, eps: float, freq_cis: torch.Tensor, plan: Union[CompressorDecodePlan, CompressorPrefillPlan], ) -> None: freq_cis = torch.view_as_real(freq_cis).flatten(-2) module = _jit_norm_rope_module(kv.dtype, kv.shape[-1], freq_cis.shape[-1]) module.forward( kv, weight, plan[1], freq_cis, int(plan.is_decode), eps, plan.compress_ratio, ) def fused_norm_rope_inplace( kv: torch.Tensor, weight: torch.Tensor, eps: float, freq_cis: torch.Tensor, positions: torch.Tensor, ) -> None: freq_cis = torch.view_as_real(freq_cis).flatten(-2) module = _jit_norm_rope_module(kv.dtype, kv.shape[-1], freq_cis.shape[-1]) module.forward( kv, weight, positions, freq_cis, 2, eps, 0, )