Serving-path speedup: state-KV LRU cache + suffix diet (see inference/bench_serve.py)
Browse files- inference/typical/native.py +101 -34
inference/typical/native.py
CHANGED
|
@@ -13,6 +13,7 @@ import copy
|
|
| 13 |
import string
|
| 14 |
|
| 15 |
import torch
|
|
|
|
| 16 |
import torch.nn as nn
|
| 17 |
import torch.nn.functional as F
|
| 18 |
|
|
@@ -262,6 +263,22 @@ def _pool_matrix(spans, offsets, base, Kmax, T, K=None):
|
|
| 262 |
return pool / pool.sum(-1, keepdim=True).clamp_min(1.0)
|
| 263 |
|
| 264 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 265 |
def _causal_pad_mask(attn2d, q_len, dtype):
|
| 266 |
"""Explicit additive (batch,1,q_len,kv_len) mask -- transformers' automatic 2D-mask +
|
| 267 |
past_key_values path mis-handles a right-padded batch sharing one KV cache; this
|
|
@@ -276,15 +293,57 @@ def _causal_pad_mask(attn2d, q_len, dtype):
|
|
| 276 |
return mask.masked_fill(~allowed.unsqueeze(1), torch.finfo(dtype).min)
|
| 277 |
|
| 278 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 279 |
@torch.inference_mode()
|
| 280 |
def native_kv_decide(head, model, state, queries, chunk: int = 32, max_state: int = 256,
|
| 281 |
-
max_suffix: int = MAX_SUFFIX, vec_cache=None
|
|
|
|
| 282 |
"""Serving path: [eos] + state encoded ONCE into a KV cache on head.backbone.model; every
|
| 283 |
query's suffix runs against that cached prefix, `chunk` queries per forward. queries =
|
| 284 |
[(query, candidates)] -> list of [K_i + 1] probability vectors (null last). Verbatim port
|
| 285 |
of native.native_kv_decide (head/model contract unchanged -- head needs only .backbone
|
| 286 |
-
(tokenizer + model) and .device; model is a NativeHead)
|
| 287 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 288 |
if model.use2:
|
| 289 |
raise NotImplementedError(
|
| 290 |
"nc_head n2/n2n3 (Qwen3-Embedding candidate vectors) isn't ported to the "
|
|
@@ -293,39 +352,47 @@ def native_kv_decide(head, model, state, queries, chunk: int = 32, max_state: in
|
|
| 293 |
"checkpoint requires it."
|
| 294 |
)
|
| 295 |
tok, dev, lm = head.backbone.tokenizer, head.device, head.backbone.model
|
| 296 |
-
|
| 297 |
-
state_cache = lm(input_ids=prefix, use_cache=True).past_key_values
|
| 298 |
-
Ls = prefix.shape[1]
|
| 299 |
out = []
|
| 300 |
for start in range(0, len(queries), chunk):
|
| 301 |
qs = queries[start:start + chunk]
|
| 302 |
m = len(qs)
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 331 |
return out
|
|
|
|
| 13 |
import string
|
| 14 |
|
| 15 |
import torch
|
| 16 |
+
from torch.profiler import record_function
|
| 17 |
import torch.nn as nn
|
| 18 |
import torch.nn.functional as F
|
| 19 |
|
|
|
|
| 263 |
return pool / pool.sum(-1, keepdim=True).clamp_min(1.0)
|
| 264 |
|
| 265 |
|
| 266 |
+
def _cache_batch_repeat_interleave(cache, repeats: int):
|
| 267 |
+
"""cache.batch_repeat_interleave(repeats), generalised to Qwen3.5's hybrid cache -- see
|
| 268 |
+
native.py's copy for the long form (its Gated-DeltaNet linear-attention layers have no
|
| 269 |
+
batch_repeat_interleave of their own; Qwen3's cache is unaffected, every layer hits the
|
| 270 |
+
first branch)."""
|
| 271 |
+
for layer in cache.layers:
|
| 272 |
+
if hasattr(layer, "batch_repeat_interleave"):
|
| 273 |
+
layer.batch_repeat_interleave(repeats)
|
| 274 |
+
continue
|
| 275 |
+
for i in range(getattr(layer, "number_of_states", 1)):
|
| 276 |
+
if layer.is_conv_states_initialized[i]:
|
| 277 |
+
layer.conv_states[i] = layer.conv_states[i].repeat_interleave(repeats, dim=0)
|
| 278 |
+
if layer.is_recurrent_states_initialized[i]:
|
| 279 |
+
layer.recurrent_states[i] = layer.recurrent_states[i].repeat_interleave(repeats, dim=0)
|
| 280 |
+
|
| 281 |
+
|
| 282 |
def _causal_pad_mask(attn2d, q_len, dtype):
|
| 283 |
"""Explicit additive (batch,1,q_len,kv_len) mask -- transformers' automatic 2D-mask +
|
| 284 |
past_key_values path mis-handles a right-padded batch sharing one KV cache; this
|
|
|
|
| 293 |
return mask.masked_fill(~allowed.unsqueeze(1), torch.finfo(dtype).min)
|
| 294 |
|
| 295 |
|
| 296 |
+
DEFAULT_MAX_OPTION_TOKENS = 24
|
| 297 |
+
|
| 298 |
+
|
| 299 |
+
def _truncate_options(tok, cand_texts: list[str], max_tokens: int | None) -> list[str]:
|
| 300 |
+
"""Cap each rendered option at max_tokens tokens (append a visible "..." if cut) --
|
| 301 |
+
"suffix diet": the K rendered options dominate suffix length, so a long option balloons
|
| 302 |
+
every subsequent forward. None/<=0 disables (no-op, returns cand_texts unchanged)."""
|
| 303 |
+
if not max_tokens or max_tokens <= 0:
|
| 304 |
+
return cand_texts
|
| 305 |
+
out = []
|
| 306 |
+
for c in cand_texts:
|
| 307 |
+
ids = tok(c, add_special_tokens=False)["input_ids"]
|
| 308 |
+
out.append(tok.decode(ids[:max_tokens]) + "..." if len(ids) > max_tokens else c)
|
| 309 |
+
return out
|
| 310 |
+
|
| 311 |
+
|
| 312 |
+
@torch.inference_mode()
|
| 313 |
+
def encode_state(head, model, state: str, max_state: int = 256):
|
| 314 |
+
"""[eos] + state -> (past_key_values, Ls), the once-per-state prefix encode that used to
|
| 315 |
+
run inside native_kv_decide on every call. Split out so a caller (Typical's state LRU)
|
| 316 |
+
can compute it once and reuse the KV cache across many decisions on the same state --
|
| 317 |
+
see native_kv_decide's `state_cache` param. Pure function of (backbone weights, state
|
| 318 |
+
text): deterministic in eval/inference_mode, so a cached entry is bit-identical to a
|
| 319 |
+
fresh encode, not just numerically close. @inference_mode here (not just relying on the
|
| 320 |
+
caller) because the resulting KV tensors get copy.deepcopy'd later (native_kv_decide's
|
| 321 |
+
per-chunk batch_repeat_interleave) -- a non-leaf, grad-tracked tensor can't be
|
| 322 |
+
deepcopy'd, so this must never run under regular autograd tracking, regardless of what
|
| 323 |
+
context the caller happens to be in."""
|
| 324 |
+
tok, dev, lm = head.backbone.tokenizer, head.device, head.backbone.model
|
| 325 |
+
with record_function("typical/tokenize_state"):
|
| 326 |
+
prefix = torch.tensor([[tok.eos_token_id] + _ids(tok, [state], max_state)[0]], device=dev)
|
| 327 |
+
with record_function("typical/state_prefix_forward"):
|
| 328 |
+
cache = lm(input_ids=prefix, use_cache=True).past_key_values
|
| 329 |
+
return cache, prefix.shape[1]
|
| 330 |
+
|
| 331 |
+
|
| 332 |
@torch.inference_mode()
|
| 333 |
def native_kv_decide(head, model, state, queries, chunk: int = 32, max_state: int = 256,
|
| 334 |
+
max_suffix: int = MAX_SUFFIX, vec_cache=None, state_cache=None,
|
| 335 |
+
max_option_tokens: int | None = DEFAULT_MAX_OPTION_TOKENS):
|
| 336 |
"""Serving path: [eos] + state encoded ONCE into a KV cache on head.backbone.model; every
|
| 337 |
query's suffix runs against that cached prefix, `chunk` queries per forward. queries =
|
| 338 |
[(query, candidates)] -> list of [K_i + 1] probability vectors (null last). Verbatim port
|
| 339 |
of native.native_kv_decide (head/model contract unchanged -- head needs only .backbone
|
| 340 |
+
(tokenizer + model) and .device; model is a NativeHead), plus two additions: `state_cache`,
|
| 341 |
+
an optional (past_key_values, Ls) pair from encode_state -- pass it to skip re-encoding
|
| 342 |
+
the state prefix (Typical.choice/score/noul/decide do this automatically via an LRU; see
|
| 343 |
+
core.py) -- and `max_option_tokens` ("suffix diet": caps each rendered option's length,
|
| 344 |
+
see _truncate_options; None/0 disables, matching the training repo's untruncated
|
| 345 |
+
behaviour exactly). No hierarchical fallback: a suffix longer than max_suffix raises
|
| 346 |
+
(raise max_suffix instead)."""
|
| 347 |
if model.use2:
|
| 348 |
raise NotImplementedError(
|
| 349 |
"nc_head n2/n2n3 (Qwen3-Embedding candidate vectors) isn't ported to the "
|
|
|
|
| 352 |
"checkpoint requires it."
|
| 353 |
)
|
| 354 |
tok, dev, lm = head.backbone.tokenizer, head.device, head.backbone.model
|
| 355 |
+
state_cache, Ls = state_cache if state_cache is not None else encode_state(head, model, state, max_state)
|
|
|
|
|
|
|
| 356 |
out = []
|
| 357 |
for start in range(0, len(queries), chunk):
|
| 358 |
qs = queries[start:start + chunk]
|
| 359 |
m = len(qs)
|
| 360 |
+
with record_function("typical/tokenize_suffix"):
|
| 361 |
+
# is_bern/yes-idx/cmask all key off candidate COUNT and the literal "yes"/"no"
|
| 362 |
+
# text (never truncated -- 1 token), so they read the original `c`; only the
|
| 363 |
+
# rendered text is diet'd.
|
| 364 |
+
is_bern = [model.noul_head == "bern" and _is_bern_row(c) for _, c in qs]
|
| 365 |
+
rendered = [RENDERS["query_only" if b else getattr(model, "render", "letters")]
|
| 366 |
+
(q, _truncate_options(tok, c, max_option_tokens))
|
| 367 |
+
for b, (q, c) in zip(is_bern, qs)]
|
| 368 |
+
enc = tok([r[0] for r in rendered], add_special_tokens=False, return_offsets_mapping=True)
|
| 369 |
+
x_ids, offs = enc["input_ids"], enc["offset_mapping"]
|
| 370 |
+
assert max(len(x) for x in x_ids) <= max_suffix, f"suffix > max_suffix={max_suffix}; raise it"
|
| 371 |
+
input_ids, am, lengths = _pack(tok, [[]] * m, x_ids, tail=[tok.eos_token_id], sink=False)
|
| 372 |
+
with record_function("typical/mask_build"):
|
| 373 |
+
T = input_ids.shape[1]
|
| 374 |
+
Kmax = max(len(c) for _, c in qs)
|
| 375 |
+
pool = torch.stack([_pool_matrix(r[1], o, 0, Kmax, T, K=len(c)) for r, o, (_, c) in zip(rendered, offs, qs)])
|
| 376 |
+
cmask = torch.zeros(m, Kmax, dtype=torch.bool)
|
| 377 |
+
for i, (_, c) in enumerate(qs):
|
| 378 |
+
cmask[i, :len(c)] = True
|
| 379 |
+
|
| 380 |
+
with record_function("typical/state_cache_deepcopy"):
|
| 381 |
+
cache = copy.deepcopy(state_cache)
|
| 382 |
+
_cache_batch_repeat_interleave(cache, m)
|
| 383 |
+
with record_function("typical/suffix_forward"):
|
| 384 |
+
attn = torch.cat([torch.ones(m, Ls, dtype=torch.long), am], dim=1).to(dev)
|
| 385 |
+
position_ids = (torch.arange(T) + Ls).expand(m, -1).to(dev)
|
| 386 |
+
H = lm(input_ids=input_ids.to(dev), attention_mask=_causal_pad_mask(attn, T, lm.dtype),
|
| 387 |
+
position_ids=position_ids, past_key_values=cache, use_cache=False).last_hidden_state.float()
|
| 388 |
+
with record_function("typical/head_forward"):
|
| 389 |
+
h = H[torch.arange(m, device=dev), (lengths - 1).to(dev)]
|
| 390 |
+
C3 = torch.bmm(pool.to(dev), H)
|
| 391 |
+
C2 = None # n2 unsupported -- guarded above
|
| 392 |
+
yi = _yes_idx([c for _, c in qs], dev) if model.noul_head == "bern" else None
|
| 393 |
+
bm = torch.tensor(is_bern, dtype=torch.bool, device=dev) if model.noul_head == "bern" else None
|
| 394 |
+
probs = torch.softmax(model(h, cmask.to(dev), C3, C2, yes_idx=yi, bern_mask=bm), dim=-1)
|
| 395 |
+
with record_function("typical/cpu_sync"):
|
| 396 |
+
for i, (_, c) in enumerate(qs):
|
| 397 |
+
out.append(torch.cat([probs[i, :len(c)], probs[i, -1:]]))
|
| 398 |
return out
|