Download code/actionman/cached.py from shing-dev/ActionMan-0.1: direct link, hf CLI and curl.
- Browser
- Download file 8.22 kB
-
https://huggingface.co/shing-dev/ActionMan-0.1/resolve/main/code/actionman/cached.py
- Command line
-
hf download hf://shing-dev/ActionMan-0.1/code/actionman/cached.py
-
curl -L -o cached.py https://huggingface.co/shing-dev/ActionMan-0.1/resolve/main/code/actionman/cached.py
8.22 kB
| """Block-causal 'cached' variant (Stage 4). | |
| Sequence = [pass-A context incl. [CALL]x4 [SEP]] [ARGS] slot block [SEP]. | |
| Mask rule: context tokens see only context; slot-block tokens see everything. | |
| Consequences: | |
| * training needs ONE forward (decision/length heads read context states that can't see the slots); | |
| * at inference the context K/V of every layer is computed once (that IS pass A) and each refinement | |
| pass runs only the ~10-40 slot tokens through the network. | |
| """ | |
| from __future__ import annotations | |
| import torch | |
| import torch.nn.functional as F | |
| from transformers.models.modernbert.modeling_modernbert import apply_rotary_pos_emb | |
| def block_masks(attn: torch.Tensor, ctx_len: torch.Tensor, window: int) -> dict: | |
| """attn [B,L] (1 = real token), ctx_len [B]. Returns HF ModernBERT mask dict (bool, True = attend).""" | |
| B, L = attn.shape | |
| idx = torch.arange(L, device=attn.device) | |
| valid_k = attn.bool()[:, None, None, :] | |
| q_slot = (idx[None, :] >= ctx_len[:, None])[:, None, :, None] | |
| k_ctx = (idx[None, :] < ctx_len[:, None])[:, None, None, :] | |
| eye = torch.eye(L, dtype=torch.bool, device=attn.device)[None, None] | |
| full = (valid_k & (q_slot | k_ctx)) | eye | |
| win = ((idx[:, None] - idx[None, :]).abs() <= window)[None, None] | |
| return {"full_attention": full, "sliding_attention": full & win} | |
| def _layer_qkv(layer, x, cos, sin): | |
| attn = layer.attn | |
| B, L, _ = x.shape | |
| qkv = attn.Wqkv(layer.attn_norm(x)).view(B, L, 3, -1, attn.head_dim) | |
| q, k, v = (t.transpose(1, 2) for t in qkv.unbind(dim=2)) | |
| q, k = apply_rotary_pos_emb(q, k, cos, sin, unsqueeze_dim=1) | |
| return q, k, v | |
| def _finish(layer, x, out): | |
| B, L = x.shape[:2] | |
| x = x + layer.attn.Wo(out.transpose(1, 2).reshape(B, L, -1)) | |
| return x + layer.mlp(layer.mlp_norm(x)) | |
| def encode_context(enc, ids: torch.Tensor): | |
| """enc = ModernBertModel. ids [1,Lc]. Returns (final hidden [1,Lc,d], per-layer (k, v) cache).""" | |
| cfg = enc.config | |
| x = enc.embeddings(input_ids=ids) | |
| L = ids.shape[1] | |
| pos = torch.arange(L, device=ids.device)[None] | |
| pe = {lt: enc.rotary_emb(x, pos, lt) for lt in set(cfg.layer_types)} | |
| idx = torch.arange(L, device=ids.device) | |
| win = ((idx[:, None] - idx[None, :]).abs() <= cfg.sliding_window)[None, None] | |
| cache = [] | |
| for layer in enc.layers: | |
| cos, sin = pe[layer.attention_type] | |
| q, k, v = _layer_qkv(layer, x, cos, sin) | |
| mask = win if layer.attention_type == "sliding_attention" else None | |
| out = F.scaled_dot_product_attention(q, k, v, attn_mask=mask, scale=layer.attn.head_dim ** -0.5) | |
| x = _finish(layer, x, out) | |
| cache.append((k, v)) | |
| return enc.final_norm(x), cache | |
| def encode_slots(enc, slot_ids: torch.Tensor, start: int, cache): | |
| """Run only the slot-block tokens (absolute positions start..start+S-1) over the cached context K/V.""" | |
| cfg = enc.config | |
| x = enc.embeddings(input_ids=slot_ids) | |
| S = slot_ids.shape[1] | |
| Lc = cache[0][0].shape[2] | |
| qpos = torch.arange(start, start + S, device=slot_ids.device) | |
| kpos = torch.cat([torch.arange(Lc, device=slot_ids.device), qpos]) | |
| pe = {lt: enc.rotary_emb(x, qpos[None], lt) for lt in set(cfg.layer_types)} | |
| win = ((qpos[:, None] - kpos[None, :]).abs() <= cfg.sliding_window)[None, None] | |
| for layer, (ck, cv) in zip(enc.layers, cache): | |
| cos, sin = pe[layer.attention_type] | |
| q, k, v = _layer_qkv(layer, x, cos, sin) | |
| k, v = torch.cat([ck, k], dim=2), torch.cat([cv, v], dim=2) | |
| mask = win if layer.attention_type == "sliding_attention" else None | |
| out = F.scaled_dot_product_attention(q, k, v, attn_mask=mask, scale=layer.attn.head_dim ** -0.5) | |
| x = _finish(layer, x, out) | |
| return enc.final_norm(x) | |
| # ---------- static-shape variants for CUDA graphs / torch.compile (v3 S6) ---------- | |
| # Lengths are padded up to a bucket and the real lengths are passed as tensors, so one compiled graph serves every | |
| # prompt in a bucket. Padded keys are masked out; outputs at padded positions are garbage and get sliced off. | |
| def _ctx_core(enc, ids, valid): | |
| """ids [1,P] padded; valid = tensor([real length]). -> hidden [1,P,d], cache.""" | |
| cfg = enc.config | |
| x = enc.embeddings(input_ids=ids) | |
| P = ids.shape[1] | |
| idx = torch.arange(P, device=ids.device) | |
| pos = idx[None] | |
| pe = {lt: enc.rotary_emb(x, pos, lt) for lt in set(cfg.layer_types)} | |
| keyok = (idx < valid)[None, None, None, :] | |
| eye = torch.eye(P, dtype=torch.bool, device=ids.device)[None, None] | |
| full = keyok | eye | |
| win = full & ((idx[:, None] - idx[None, :]).abs() <= cfg.sliding_window)[None, None] | |
| cache = [] | |
| for layer in enc.layers: | |
| cos, sin = pe[layer.attention_type] | |
| q, k, v = _layer_qkv(layer, x, cos, sin) | |
| mask = win if layer.attention_type == "sliding_attention" else full | |
| out = F.scaled_dot_product_attention(q, k, v, attn_mask=mask, scale=layer.attn.head_dim ** -0.5) | |
| x = _finish(layer, x, out) | |
| cache.append((k, v)) | |
| return enc.final_norm(x), cache | |
| def _slot_core(enc, slot_ids, start, s_valid, c_valid, cache): | |
| """slot_ids [1,Sp] padded; start/s_valid/c_valid tensors; cache from _ctx_core (padded context).""" | |
| cfg = enc.config | |
| x = enc.embeddings(input_ids=slot_ids) | |
| Sp = slot_ids.shape[1] | |
| Lc = cache[0][0].shape[2] | |
| ar = torch.arange(Sp, device=slot_ids.device) | |
| qpos = start + ar | |
| kc = torch.arange(Lc, device=slot_ids.device) | |
| kpos = torch.cat([kc, qpos]) | |
| keyok = torch.cat([kc < c_valid, ar < s_valid])[None, None, None, :] | |
| pe = {lt: enc.rotary_emb(x, qpos[None], lt) for lt in set(cfg.layer_types)} | |
| full = keyok | torch.cat([torch.zeros(Sp, Lc, dtype=torch.bool, device=slot_ids.device), | |
| torch.eye(Sp, dtype=torch.bool, device=slot_ids.device)], dim=1)[None, None] | |
| win = full & ((qpos[:, None] - kpos[None, :]).abs() <= cfg.sliding_window)[None, None] | |
| for layer, (ck, cv) in zip(enc.layers, cache): | |
| cos, sin = pe[layer.attention_type] | |
| q, k, v = _layer_qkv(layer, x, cos, sin) | |
| k, v = torch.cat([ck, k], dim=2), torch.cat([cv, v], dim=2) | |
| mask = win if layer.attention_type == "sliding_attention" else full | |
| out = F.scaled_dot_product_attention(q, k, v, attn_mask=mask, scale=layer.attn.head_dim ** -0.5) | |
| x = _finish(layer, x, out) | |
| return enc.final_norm(x) | |
| def _bucket(n: int, b: int) -> int: | |
| return -(-n // b) * b | |
| def make_static(enc, ctx_bucket: int = 256, slot_bucket: int = 128, compile: bool = True): | |
| """-> (encode_context, encode_slots) drop-ins that pad to buckets (and run compiled, CUDA-graphed cores). | |
| Buckets are coarse on purpose: every (context bucket, slot bucket) pair is a separate compiled graph.""" | |
| ctx_core, slot_core = _ctx_core, _slot_core | |
| if compile: | |
| import torch._dynamo as dynamo | |
| for name in ("cache_size_limit", "recompile_limit"): | |
| if hasattr(dynamo.config, name): | |
| setattr(dynamo.config, name, 64) | |
| ctx_core = torch.compile(_ctx_core, mode="reduce-overhead", dynamic=False, fullgraph=False) | |
| slot_core = torch.compile(_slot_core, mode="reduce-overhead", dynamic=False, fullgraph=False) | |
| pad_id = enc.config.pad_token_id or 0 | |
| state = {} | |
| def encode_context_s(enc_, ids): | |
| L = ids.shape[1] | |
| P = _bucket(L, ctx_bucket) | |
| ids_p = F.pad(ids, (0, P - L), value=pad_id) | |
| h, cache = ctx_core(enc, ids_p, torch.tensor(L, device=ids.device)) | |
| h, cache = h.clone(), [(k.clone(), v.clone()) for k, v in cache] # outlive the CUDA-graph buffers | |
| state["c_valid"] = L | |
| return h[:, :L], cache | |
| def encode_slots_s(enc_, slot_ids, start, cache): | |
| S = slot_ids.shape[1] | |
| Sp = _bucket(S, slot_bucket) | |
| ids_p = F.pad(slot_ids, (0, Sp - S), value=pad_id) | |
| dev = slot_ids.device | |
| out = slot_core(enc, ids_p, torch.tensor(start, device=dev), torch.tensor(S, device=dev), | |
| torch.tensor(state["c_valid"], device=dev), cache) | |
| return out[:, :S].clone() | |
| return encode_context_s, encode_slots_s | |