guychuk commited on
Commit
c552a5b
·
verified ·
1 Parent(s): a9b9a8e

Serving-path speedup: state-KV LRU cache + suffix diet (see inference/bench_serve.py)

Browse files
Files changed (1) hide show
  1. 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). No hierarchical fallback: a
287
- suffix longer than max_suffix raises (raise max_suffix instead)."""
 
 
 
 
 
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
- prefix = torch.tensor([[tok.eos_token_id] + _ids(tok, [state], max_state)[0]], device=dev)
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
- is_bern = [model.noul_head == "bern" and _is_bern_row(c) for _, c in qs]
304
- rendered = [RENDERS["query_only" if b else getattr(model, "render", "letters")](q, c)
305
- for b, (q, c) in zip(is_bern, qs)]
306
- enc = tok([r[0] for r in rendered], add_special_tokens=False, return_offsets_mapping=True)
307
- x_ids, offs = enc["input_ids"], enc["offset_mapping"]
308
- assert max(len(x) for x in x_ids) <= max_suffix, f"suffix > max_suffix={max_suffix}; raise it"
309
- input_ids, am, lengths = _pack(tok, [[]] * m, x_ids, tail=[tok.eos_token_id], sink=False)
310
- T = input_ids.shape[1]
311
- Kmax = max(len(c) for _, c in qs)
312
- pool = torch.stack([_pool_matrix(r[1], o, 0, Kmax, T, K=len(c)) for r, o, (_, c) in zip(rendered, offs, qs)])
313
- cmask = torch.zeros(m, Kmax, dtype=torch.bool)
314
- for i, (_, c) in enumerate(qs):
315
- cmask[i, :len(c)] = True
316
-
317
- cache = copy.deepcopy(state_cache)
318
- cache.batch_repeat_interleave(m)
319
- attn = torch.cat([torch.ones(m, Ls, dtype=torch.long), am], dim=1).to(dev)
320
- position_ids = (torch.arange(T) + Ls).expand(m, -1).to(dev)
321
- H = lm(input_ids=input_ids.to(dev), attention_mask=_causal_pad_mask(attn, T, lm.dtype),
322
- position_ids=position_ids, past_key_values=cache, use_cache=False).last_hidden_state.float()
323
- h = H[torch.arange(m, device=dev), (lengths - 1).to(dev)]
324
- C3 = torch.bmm(pool.to(dev), H)
325
- C2 = None # n2 unsupported -- guarded above
326
- yi = _yes_idx([c for _, c in qs], dev) if model.noul_head == "bern" else None
327
- bm = torch.tensor(is_bern, dtype=torch.bool, device=dev) if model.noul_head == "bern" else None
328
- probs = torch.softmax(model(h, cmask.to(dev), C3, C2, yes_idx=yi, bern_mask=bm), dim=-1)
329
- for i, (_, c) in enumerate(qs):
330
- out.append(torch.cat([probs[i, :len(c)], probs[i, -1:]]))
 
 
 
 
 
 
 
 
 
 
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