Skip to content

Commit ebd0cab

Browse files
jomcgiclaude
andcommitted
feat: port upstream fixes (lm_head rows, GDN conv sync, PLE hash, oracle stat)
Ports from upstream FreeToken, adapted to the tier: FlashML-org#342 lm_head on sampled rows only (already generalised here via select_lm_head_rows); FlashML-org#339 the varlen GDN/KDA prefill conv takes max_seq_len from the scheduler on the Triton fallback (inert when sgl_kernel is installed, which every install path pins, so no node-4 change); FlashML-org#338 the n-gram PLE row-id hash as one Triton kernel with a bounded memo that is bypassed during CUDA graph capture (consumed by the pinned and cached PLE backends; the disk backend stages from its host hash); FlashML-org#231 the routing-oracle hit rate on the stats line next to the realised hot-pair rate, with the baseline reset on a live cache rebuild so the oracle can never read below realised. FlashML-org#89 (route-density tile selection) is skipped: its ds_fp4 tile table does not match the NVFP4 kernel's, which needs its own sm_89 sweep. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A88MCbnLtwsFSHmqwuJezY
1 parent abd2056 commit ebd0cab

14 files changed

Lines changed: 784 additions & 11 deletions

File tree

python/freetoken/attention/linear.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,17 @@ class FLAMetadata:
2626
fresh_state_indices prefill only: the state-pool slots whose sequence is fresh
2727
(cached_len == 0) and must be zeroed before the chunk kernel
2828
reads them in place. None if there are none / for decode.
29+
max_seq_len prefill only: the longest extend_len as a host int. It reaches
30+
only the Triton fallback, where it sizes the varlen convolution
31+
launch without a device-to-host sync, and is ignored when
32+
sgl_kernel is installed.
2933
"""
3034

3135
cu_seqlens: torch.Tensor
3236
cache_indices: torch.Tensor
3337
has_initial_state: torch.Tensor | None = None
3438
fresh_state_indices: torch.Tensor | None = None
39+
max_seq_len: int | None = None
3540

3641
# --- hybrid-radix track-checkpoint (extra_buffer) fields; all None when not caching ---
3742
# For each request crossing a chunk-aligned (×CHUNK) boundary this forward, snapshot its
@@ -87,6 +92,7 @@ def gdn_slot(r):
8792
fresh_state_indices=(
8893
fresh_host.to(device, non_blocking=True) if fresh_host is not None else None
8994
),
95+
max_seq_len=max(lens),
9096
**track,
9197
)
9298

python/freetoken/kernel/backend.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ def is_sgl_kernel_installed() -> bool:
3131
return _importable("sgl_kernel")
3232

3333

34+
@functools.cache
35+
def is_triton_installed() -> bool:
36+
"""Whether the Triton runtime used by ``freetoken.kernel.triton`` is available."""
37+
return _importable("triton")
38+
39+
3440
@functools.cache
3541
def is_triton_kernels_installed() -> bool:
3642
"""OpenAI's ``triton_kernels`` (the fused MoE router used by ``moe.fused.fused_topk``).

python/freetoken/kernel/causal_conv1d.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,14 @@ def causal_conv1d_varlen(
2222
cu_seqlens: torch.Tensor, # [batch+1] int32 prefix sums of per-request lengths
2323
cache_indices: torch.Tensor, # [batch] int32 slot id per request
2424
has_initial_state: torch.Tensor, # [batch] bool (carry conv state across chunks)
25+
max_seq_len: int | None = None, # host-known longest extend_len
2526
) -> torch.Tensor:
2627
"""Varlen (prefill) depthwise causal conv with silu; writes silu(conv) into ``x``
27-
in place and refreshes ``conv_states[cache_indices]`` with each request's tail."""
28+
in place and refreshes ``conv_states[cache_indices]`` with each request's tail.
29+
30+
``max_seq_len`` only sizes the Triton launch grid. Passing the scheduler's host value
31+
avoids the device-to-host sync otherwise needed to derive it from ``cu_seqlens``.
32+
"""
2833
from freetoken.kernel.backend import is_sgl_kernel_installed
2934

3035
if not is_sgl_kernel_installed():
@@ -33,7 +38,13 @@ def causal_conv1d_varlen(
3338
)
3439

3540
return triton_causal_conv1d_varlen(
36-
x, weight, conv_states, cu_seqlens, cache_indices, has_initial_state
41+
x,
42+
weight,
43+
conv_states,
44+
cu_seqlens,
45+
cache_indices,
46+
has_initial_state,
47+
max_seq_len=max_seq_len,
3748
)
3849

3950
from sgl_kernel import causal_conv1d_fwd
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
"""Fused n-gram hash to PLE table row ids.
3+
4+
The eager hash builds a packed ragged window, scans boundaries, and launches many
5+
small elementwise operations. This kernel performs the same signed int64 arithmetic
6+
with one Triton program per token and without materializing the packed window.
7+
"""
8+
9+
from __future__ import annotations
10+
11+
import torch
12+
import triton
13+
import triton.language as tl
14+
15+
16+
@triton.jit
17+
def _ple_row_ids_kernel(
18+
ids_ptr,
19+
ctx_ptr,
20+
req_ptr,
21+
local_ptr,
22+
mult_ptr,
23+
vocab_ptr,
24+
off_ptr,
25+
out_ptr,
26+
EOS: tl.constexpr,
27+
CTX_LEN: tl.constexpr,
28+
NGRAM: tl.constexpr,
29+
HEADS_PER: tl.constexpr,
30+
NUM_HEADS: tl.constexpr,
31+
BLOCK_H: tl.constexpr,
32+
):
33+
token = tl.program_id(0).to(tl.int64)
34+
req = tl.load(req_ptr + token).to(tl.int64)
35+
local = tl.load(local_ptr + token).to(tl.int64)
36+
37+
head = tl.arange(0, BLOCK_H)
38+
head_ok = head < NUM_HEADS
39+
mixed = tl.load(ids_ptr + token).to(tl.int64) * tl.load(mult_ptr).to(tl.int64)
40+
acc = tl.zeros([BLOCK_H], dtype=tl.int64)
41+
42+
valid = 1
43+
for shift in tl.static_range(1, NGRAM):
44+
column = CTX_LEN + local - shift
45+
from_ids = column >= CTX_LEN
46+
from_ctx = (column >= 0) & (column < CTX_LEN)
47+
token_ids = tl.load(ids_ptr + (token - shift), mask=from_ids, other=0)
48+
token_ctx = tl.load(ctx_ptr + req * CTX_LEN + column, mask=from_ctx, other=EOS)
49+
raw = tl.where(from_ids, token_ids, token_ctx).to(tl.int64)
50+
valid = valid * tl.where((column >= 0) & (raw != EOS), 1, 0)
51+
mixed = mixed ^ (
52+
tl.where(valid == 1, raw, EOS) * tl.load(mult_ptr + shift).to(tl.int64)
53+
)
54+
ngram = shift + 1
55+
block = (head >= (ngram - 2) * HEADS_PER) & (head < (ngram - 1) * HEADS_PER)
56+
acc = tl.where(block, mixed, acc)
57+
58+
vocab = tl.load(vocab_ptr + head, mask=head_ok, other=1).to(tl.int64)
59+
offset = tl.load(off_ptr + head, mask=head_ok, other=0).to(tl.int64)
60+
rem = acc % vocab
61+
rem = tl.where(rem < 0, rem + vocab, rem)
62+
tl.store(out_ptr + token * NUM_HEADS + head, rem + offset, mask=head_ok)
63+
64+
65+
def ple_row_ids(
66+
input_ids: torch.Tensor,
67+
ngram_context: torch.Tensor,
68+
req_index: torch.Tensor,
69+
local_index: torch.Tensor,
70+
multipliers: torch.Tensor,
71+
vocab_sizes: torch.Tensor,
72+
offsets: torch.Tensor,
73+
*,
74+
eos_token_id: int,
75+
heads_per_ngram: int,
76+
) -> torch.Tensor:
77+
"""Return ``[tokens, heads]`` int64 global PLE table row ids."""
78+
tokens = input_ids.numel()
79+
ngram_size = int(multipliers.numel())
80+
num_heads = int(vocab_sizes.numel())
81+
ctx_len = int(ngram_context.shape[-1])
82+
if ctx_len != ngram_size - 1:
83+
raise ValueError(
84+
f"PLE hash context has {ctx_len} ids but ngram_size {ngram_size} "
85+
f"needs {ngram_size - 1}"
86+
)
87+
if num_heads != heads_per_ngram * (ngram_size - 1):
88+
raise ValueError(
89+
f"PLE hash has {num_heads} heads, expected heads_per_ngram "
90+
f"{heads_per_ngram} x {ngram_size - 1} n-gram orders"
91+
)
92+
out = torch.empty(
93+
(tokens, num_heads), dtype=torch.int64, device=input_ids.device
94+
)
95+
if tokens == 0:
96+
return out
97+
assert ngram_context.is_contiguous()
98+
assert out.is_contiguous()
99+
_ple_row_ids_kernel[(tokens,)](
100+
input_ids,
101+
ngram_context,
102+
req_index,
103+
local_index,
104+
multipliers,
105+
vocab_sizes,
106+
offsets,
107+
out,
108+
EOS=int(eos_token_id),
109+
CTX_LEN=ctx_len,
110+
NGRAM=ngram_size,
111+
HEADS_PER=int(heads_per_ngram),
112+
NUM_HEADS=num_heads,
113+
BLOCK_H=triton.next_power_of_2(num_heads),
114+
num_warps=1,
115+
)
116+
return out
117+
118+
119+
__all__ = ["ple_row_ids"]

python/freetoken/models/glm5_next/linear.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ def _conv_prefill(self, conv_in, pool, fla):
102102
fla.cu_seqlens,
103103
fla.cache_indices,
104104
fla.has_initial_state,
105+
max_seq_len=fla.max_seq_len,
105106
)
106107
return out.transpose(0, 1)
107108

python/freetoken/models/qwen3_5_moe/gdn.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -113,14 +113,19 @@ def _gate_params(self, a: torch.Tensor, b: torch.Tensor):
113113
def _conv_weight(self) -> torch.Tensor:
114114
return self.conv1d.weight.squeeze(1) # [conv_dim, kernel] for the fused kernel
115115

116-
def _conv_prefill(self, conv_in, pool, cu_seqlens, cache_indices, has_initial_state) -> torch.Tensor:
116+
def _conv_prefill(
117+
self, conv_in, pool, cu_seqlens, cache_indices, has_initial_state, max_seq_len=None
118+
) -> torch.Tensor:
117119
"""Varlen causal conv (fused sgl_kernel) with silu; reads/updates each request's
118120
conv state in place by ``cache_indices`` slot. ``conv_in`` [total, conv_dim].
119-
``cu_seqlens`` / ``cache_indices`` / ``has_initial_state`` come from FLAMetadata."""
121+
``cu_seqlens`` / ``cache_indices`` / ``has_initial_state`` come from
122+
``FLAMetadata``. Its host-known ``max_seq_len`` lets the Triton fallback size
123+
its launch without a sync."""
120124
li = pool.local_index(self.layer_id)
121125
x = conv_in.transpose(0, 1).contiguous() # [conv_dim, total]
122126
out = causal_conv1d_varlen(x, self._conv_weight(), pool.conv_states[li],
123-
cu_seqlens, cache_indices, has_initial_state)
127+
cu_seqlens, cache_indices, has_initial_state,
128+
max_seq_len=max_seq_len)
124129
return out.transpose(0, 1) # [total, conv_dim]
125130

126131
def _conv_decode(self, conv_in: torch.Tensor, table_idx: torch.Tensor, pool) -> torch.Tensor:
@@ -189,7 +194,8 @@ def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
189194
)
190195
else:
191196
mixed = self._conv_prefill(
192-
conv_in, pool, fla.cu_seqlens, fla.cache_indices, fla.has_initial_state)
197+
conv_in, pool, fla.cu_seqlens, fla.cache_indices,
198+
fla.has_initial_state, fla.max_seq_len)
193199
# fla chunk handles GQA in-kernel: q/k stay at num_k_heads, v at num_v_heads.
194200
qf, kf, vf = torch.split(mixed, [self.key_dim, self.key_dim, self.value_dim], dim=-1)
195201
q = qf.reshape(1, total, self.num_k_heads, self.head_k_dim).to(dtype)

python/freetoken/models/qwen4_exp/gdn.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -122,14 +122,19 @@ def _gate_params(self, a: torch.Tensor, b: torch.Tensor):
122122
def _conv_weight(self) -> torch.Tensor:
123123
return self.conv1d.weight.squeeze(1) # [conv_dim, kernel] for the fused kernel
124124

125-
def _conv_prefill(self, conv_in, pool, cu_seqlens, cache_indices, has_initial_state) -> torch.Tensor:
125+
def _conv_prefill(
126+
self, conv_in, pool, cu_seqlens, cache_indices, has_initial_state, max_seq_len=None
127+
) -> torch.Tensor:
126128
"""Varlen causal conv (fused sgl_kernel) with silu; reads/updates each request's
127129
conv state in place by ``cache_indices`` slot. ``conv_in`` [total, conv_dim].
128-
``cu_seqlens`` / ``cache_indices`` / ``has_initial_state`` come from FLAMetadata."""
130+
``cu_seqlens`` / ``cache_indices`` / ``has_initial_state`` come from
131+
``FLAMetadata``. Its host-known ``max_seq_len`` lets the Triton fallback size
132+
its launch without a sync."""
129133
li = pool.local_index(self.layer_id)
130134
x = conv_in.transpose(0, 1).contiguous() # [conv_dim, total]
131135
out = causal_conv1d_varlen(x, self._conv_weight(), pool.conv_states[li],
132-
cu_seqlens, cache_indices, has_initial_state)
136+
cu_seqlens, cache_indices, has_initial_state,
137+
max_seq_len=max_seq_len)
133138
return out.transpose(0, 1) # [total, conv_dim]
134139

135140
def _conv_decode(self, conv_in: torch.Tensor, table_idx: torch.Tensor, pool) -> torch.Tensor:
@@ -228,7 +233,8 @@ def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
228233
)
229234
else:
230235
mixed = self._conv_prefill(
231-
conv_in, pool, fla.cu_seqlens, fla.cache_indices, fla.has_initial_state)
236+
conv_in, pool, fla.cu_seqlens, fla.cache_indices,
237+
fla.has_initial_state, fla.max_seq_len)
232238
# fla chunk handles GQA in-kernel: q/k stay at num_k_heads, v at num_v_heads.
233239
qf, kf, vf = torch.split(mixed, [self.key_dim, self.key_dim, self.value_dim], dim=-1)
234240
q = qf.reshape(1, total, self.num_k_heads, self.head_k_dim).to(dtype)

python/freetoken/models/qwen4_exp/ple.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,21 @@
5353
_SPLITMIX_M2 = 0x94D049BB133111EB
5454
_PLE_LAYER_PRIME = 10007
5555
_PLE_CACHE_SLAB_ROWS = 1 << 16
56+
_TOKEN_INDEX_CACHE_SIZE = 64
57+
_FUSED_HASH_ENV = "FREETOKEN_PLE_FUSED_HASH"
5658

5759
logger = init_logger(__name__)
5860

5961

62+
def _fused_row_ids_enabled() -> bool:
63+
"""Enable the fused hash unless the environment explicitly disables it."""
64+
return (os.getenv(_FUSED_HASH_ENV) or "1").strip() not in (
65+
"0",
66+
"false",
67+
"False",
68+
)
69+
70+
6071
class _IOVec(ctypes.Structure):
6172
_fields_ = [("base", ctypes.c_void_p), ("length", ctypes.c_size_t)]
6273

@@ -2644,6 +2655,7 @@ def __init__(self, args: Qwen4ExpArgs, table: PLETableBackend | None = None) ->
26442655
self._table = table
26452656
self._host_hash_constants: tuple[torch.Tensor, torch.Tensor, torch.Tensor] | None = None
26462657
self._host_hash_buffers: tuple[torch.Tensor, ...] | None = None
2658+
self._token_index_cache: dict[tuple, Tuple[torch.Tensor, torch.Tensor]] = {}
26472659

26482660
def attach_table(self, table: PLETableBackend) -> None:
26492661
self._table = table
@@ -2775,8 +2787,84 @@ def _shift_ignore_eos(self, packed: torch.Tensor) -> List[torch.Tensor]:
27752787
shifted.append(torch.where(valid, gathered, packed.new_full((), self.eos_token_id)))
27762788
return shifted
27772789

2790+
def _token_index(self, meta: PLEMetadata) -> Tuple[torch.Tensor, torch.Tensor]:
2791+
"""Return each token's request index and request-local offset."""
2792+
device = meta.input_ids.device
2793+
num_tokens = meta.input_ids.numel()
2794+
capturing = device.type == "cuda" and torch.cuda.is_current_stream_capturing()
2795+
key = (
2796+
meta.is_decode,
2797+
(num_tokens,) if meta.is_decode else tuple(meta.seq_lens),
2798+
str(device),
2799+
)
2800+
# A graph must own its index allocations. Reusing an eager memo entry here
2801+
# lets normal cache eviction free storage whose pointers were baked into the
2802+
# captured Triton launch.
2803+
cached = None if capturing else self._token_index_cache.get(key)
2804+
if cached is not None:
2805+
return cached
2806+
if meta.is_decode:
2807+
index = (
2808+
torch.arange(num_tokens, dtype=torch.int32, device=device),
2809+
torch.zeros(num_tokens, dtype=torch.int32, device=device),
2810+
)
2811+
else:
2812+
cu = meta.cu_seqlens.long()
2813+
flat_pos = torch.arange(num_tokens, device=device)
2814+
req = (torch.searchsorted(cu, flat_pos, right=True) - 1).clamp_(
2815+
max=len(meta.seq_lens) - 1
2816+
)
2817+
index = (
2818+
req.to(torch.int32),
2819+
(flat_pos - cu[req]).to(torch.int32),
2820+
)
2821+
if not capturing:
2822+
if len(self._token_index_cache) >= _TOKEN_INDEX_CACHE_SIZE:
2823+
self._token_index_cache.pop(next(iter(self._token_index_cache)))
2824+
self._token_index_cache[key] = index
2825+
return index
2826+
2827+
def _use_fused_row_ids(self, meta: PLEMetadata) -> bool:
2828+
from freetoken.kernel.backend import is_triton_installed
2829+
2830+
device = meta.input_ids.device
2831+
if (
2832+
device.type != "cuda"
2833+
or not is_triton_installed()
2834+
or not _fused_row_ids_enabled()
2835+
):
2836+
return False
2837+
return all(
2838+
tensor.device == device
2839+
for tensor in (
2840+
meta.ngram_context,
2841+
self.layer_multipliers,
2842+
self.ngram_heads_vocab_sizes,
2843+
self.ngram_heads_offsets,
2844+
)
2845+
)
2846+
27782847
def row_ids(self, meta: PLEMetadata) -> torch.Tensor:
27792848
"""Global table row per (token, hash head): ``[T, num_ngram_heads]`` int64."""
2849+
if self._use_fused_row_ids(meta):
2850+
from freetoken.kernel.triton.ple_hash import ple_row_ids
2851+
2852+
req, local = self._token_index(meta)
2853+
return ple_row_ids(
2854+
meta.input_ids.long(),
2855+
meta.ngram_context,
2856+
req,
2857+
local,
2858+
self.layer_multipliers,
2859+
self.ngram_heads_vocab_sizes,
2860+
self.ngram_heads_offsets,
2861+
eos_token_id=self.eos_token_id,
2862+
heads_per_ngram=self.heads_per_ngram,
2863+
)
2864+
return self.row_ids_reference(meta)
2865+
2866+
def row_ids_reference(self, meta: PLEMetadata) -> torch.Tensor:
2867+
"""Torch implementation retained as the CPU path and fused-kernel oracle."""
27802868
packed, select = self._window(meta)
27812869
tokens = [select(s) for s in self._shift_ignore_eos(packed)]
27822870
blocks = []

0 commit comments

Comments
 (0)