| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033 |
- # SPDX-License-Identifier: Apache-2.0
- # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
- """Engram: n-gram hash lookups gated into the hyper-connection stream.
- Port of the reference ``inference/engram.py`` + ``Engram`` /
- ``ParallelEngramEmbedding`` from ``inference/model.py`` (DeepSeek V4.1
- checkpoint layout). Engram modules live on the backbone layers listed in
- ``engram_layer_ids`` only.
- Two pieces of cross-forward state are needed because vLLM streams tokens
- chunk-by-chunk while an n-gram at position ``p`` needs the token ids at
- ``p-1..p-3``:
- - ``token_map``: token id -> compressed vocab id, built once from the
- model's tokenizer at init (deterministic; asserted against
- ``engram_compressed_vocab_size``).
- - ``hash_cache``: one int32 slot per KV slot of the first local layer's
- sliding-window cache, holding the compressed id (or DEAD) of the token
- last written to that slot. Slots are stable per (request, position) —
- the block table pins a position to a physical slot, prefix-cache hits
- reuse both the physical blocks and the identical token ids, and
- spec-decode rollbacks rewrite the same slots — so lookbacks read back
- exactly what the owning request wrote. Lookback depth (3) is far inside
- the sliding window (128), so window eviction never frees a block a
- live lookback still needs.
- Slots are not part of the KV cache, so KV loaded from another instance
- (P/D, offload connectors) leaves them unwritten. The runner therefore
- passes ``lookback_token_ids``, the ids just before each request's chunk
- start, which take precedence over the slots. The V2 runner reads them
- from its device-resident token history and needs no slot cache; the V1
- runner's CPU token table holds placeholders for generated tokens under
- async scheduling, so it passes prompt positions only and keeps the slot
- cache for the rest.
- """
- import weakref
- import numpy as np
- import torch
- from torch import nn
- from vllm.config import VllmConfig, get_current_vllm_config
- from vllm.distributed import (
- get_tensor_model_parallel_rank,
- get_tensor_model_parallel_world_size,
- tensor_model_parallel_all_gather,
- )
- from vllm.logger import init_logger
- from vllm.model_executor.layers.linear import ReplicatedLinear
- from vllm.model_executor.layers.quantization import QuantizationConfig
- from vllm.model_executor.utils import set_weight_attrs
- from vllm.triton_utils import tl, triton
- from vllm.utils.platform_utils import is_uva_available
- from vllm.utils.torch_utils import get_accelerator_view_from_cpu_tensor
- logger = init_logger(__name__)
- # Cache value for tokens that take no part in an n-gram (image spans).
- DEAD_ID = -1
- def _is_prime(n: int) -> bool:
- """Deterministic Miller-Rabin for n < 2**32 (avoids a sympy import)."""
- if n < 2:
- return False
- for p in (2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37):
- if n % p == 0:
- return n == p
- d = n - 1
- r = 0
- while d % 2 == 0:
- d //= 2
- r += 1
- for a in (2, 7, 61):
- x = pow(a, d, n)
- if x in (1, n - 1):
- continue
- for _ in range(r - 1):
- x = x * x % n
- if x == n - 1:
- break
- else:
- return False
- return True
- def find_next_prime(start: int, seen_primes: set[int]) -> int:
- """The smallest prime above `start` that has not been handed out yet."""
- candidate = start + 1
- while not _is_prime(candidate) or candidate in seen_primes:
- candidate += 1
- return candidate
- def build_compressed_token_map(tokenizer) -> tuple[list[int], int]:
- """Map every token id onto a smaller id space where tokens that normalize
- alike collapse together.
- N-grams are hashed over these compressed ids, so " The", "the" and "THE"
- all hash the same way. The compressed size matters beyond bounds checking:
- every hash multiplier is derived from it.
- """
- from tokenizers import Regex, normalizers
- # A private-use char, so a token that is exactly one space survives
- # Strip() instead of collapsing to the empty string and merging with
- # unrelated tokens.
- sentinel = "\ue000"
- normalizer = normalizers.Sequence(
- [
- normalizers.NFKC(),
- normalizers.NFD(),
- normalizers.StripAccents(),
- normalizers.Lowercase(),
- normalizers.Replace(Regex(r"[ \t\r\n]+"), " "),
- normalizers.Replace(Regex(r"^ $"), sentinel),
- normalizers.Strip(),
- normalizers.Replace(sentinel, " "),
- ]
- )
- # The raw Rust tokenizer, matching what training decodes with
- # (no clean_up_tokenization_spaces).
- backend = tokenizer.backend_tokenizer
- key_to_new: dict[str, int] = {}
- lookup = [0] * len(tokenizer)
- for token_id in range(len(tokenizer)):
- text = backend.decode([token_id], skip_special_tokens=False)
- if "\ufffd" in text:
- # A partial UTF-8 byte token: nothing to normalize, so key it
- # by its raw form.
- key = backend.id_to_token(token_id)
- else:
- normalized = normalizer.normalize_str(text)
- key = normalized if normalized else text
- new_id = key_to_new.get(key)
- if new_id is None:
- new_id = len(key_to_new)
- key_to_new[key] = new_id
- lookup[token_id] = new_id
- return lookup, len(key_to_new)
- def compute_hash_multipliers(
- layer_ids: tuple[int, ...], max_ngram_size: int, compressed_vocab_size: int
- ) -> torch.Tensor:
- """One multiplier per (layer, lookback), from a per-layer RNG so layers
- hash differently. Kept odd and bounded so `token_id * multiplier` cannot
- overflow int64.
- """
- max_long = np.iinfo(np.int64).max
- multiplier_bound = max(1, (max_long // compressed_vocab_size) // 2)
- rows = []
- for layer_id in layer_ids:
- generator = np.random.default_rng(10007 * layer_id)
- values = generator.integers(
- low=0,
- high=multiplier_bound,
- size=(max_ngram_size,),
- dtype=np.int64,
- )
- rows.append(torch.tensor(values * 2 + 1))
- return torch.stack(rows)
- class EngramLayout:
- """Bucket layout of the n-gram hash tables.
- A position is hashed as `max_ngram_size - 1` n-grams (2-gram .. max), each
- split over `n_heads` heads. Every (n-gram size, head) pair owns its own
- prime-sized bucket range in the layer's table; the primes are drawn in
- order and never reused, which keeps the ranges disjoint.
- """
- def __init__(self, config) -> None:
- self.layer_ids: tuple[int, ...] = tuple(config.engram_layer_ids)
- self.num_embeddings: tuple[int, ...] = tuple(config.engram_num_embeddings)
- self.max_ngram_size: int = config.engram_max_ngram_size
- self.n_heads: int = config.engram_n_heads
- self.head_dim: int = config.engram_head_dim
- self.compressed_vocab_size: int = config.engram_compressed_vocab_size
- self.pad_token_id: int = config.engram_pad_token_id
- assert len(self.layer_ids) == len(self.num_embeddings)
- primes = []
- seen: set[int] = set()
- for _ in self.layer_ids:
- per_ngram = []
- for _ in range(self.max_ngram_size - 1):
- sizes, current = [], config.engram_vocab_size - 1
- for _ in range(self.n_heads):
- current = find_next_prime(current, seen)
- seen.add(current)
- sizes.append(current)
- per_ngram.append(tuple(sizes))
- primes.append(tuple(per_ngram))
- self.primes: tuple[tuple[tuple[int, ...], ...], ...] = tuple(primes)
- self.n_hash_cols = (self.max_ngram_size - 1) * self.n_heads
- flat = [[p for per_ngram in layer for p in per_ngram] for layer in primes]
- offsets = [np.cumsum([0, *sizes[:-1]]) for sizes in flat]
- self.offsets = torch.tensor(np.array(offsets)) # [n_layers, n_hash_cols]
- @classmethod
- def from_config(cls, config) -> "EngramLayout | None":
- if not getattr(config, "engram_layer_ids", None):
- return None
- return cls(config)
- @triton.jit(do_not_specialize=["num_tokens"])
- def _write_hash_cache_kernel(
- input_ids,
- token_map,
- dead_mask,
- slot_mapping,
- cache,
- num_tokens,
- input_stride,
- mask_stride,
- slot_stride,
- BLOCK_SIZE: tl.constexpr,
- dead_id,
- ):
- token_idx = tl.program_id(0) * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE)
- slot = tl.load(
- slot_mapping + token_idx * slot_stride, token_idx < num_tokens, other=-1
- ).to(tl.int64)
- valid = (token_idx < num_tokens) & (slot >= 0)
- token = tl.load(input_ids + token_idx * input_stride, valid, other=0)
- value = tl.load(token_map + token, valid, other=0)
- dead = tl.load(dead_mask + token_idx * mask_stride, valid, other=False)
- value = tl.where(dead, dead_id, value)
- tl.store(cache + slot, value, valid)
- @triton.jit(
- do_not_specialize=[
- "num_tokens",
- "num_slots",
- "num_query_rows",
- "num_table_rows",
- "max_blocks",
- ]
- )
- def _hash_ids_kernel(
- input_ids,
- token_map,
- dead_mask,
- positions,
- block_table,
- query_start_loc,
- multipliers,
- primes,
- offsets,
- cache,
- lookback_token_ids,
- lookback_dead_mask,
- output,
- num_tokens,
- num_slots,
- pad_id,
- input_stride,
- mask_stride,
- position_stride,
- table_stride,
- table_col_stride,
- query_stride,
- num_query_rows,
- num_table_rows,
- max_blocks,
- cache_block_size,
- MAX_NGRAM: tl.constexpr,
- num_heads,
- BLOCK_T: tl.constexpr,
- BLOCK_H: tl.constexpr,
- dead_id,
- lookback_depth,
- lookback_row_stride,
- lookback_col_stride,
- lookback_mask_row_stride,
- lookback_mask_col_stride,
- ):
- token = tl.program_id(0) * BLOCK_T + tl.arange(0, BLOCK_T)
- layer = tl.program_id(1)
- num_layers = tl.num_programs(1)
- valid = token < num_tokens
- # Upper bound in query_start_loc[1:], including repeated padding boundaries.
- lo = tl.full((BLOCK_T,), 0, tl.int32)
- hi = tl.full((BLOCK_T,), num_query_rows, tl.int32)
- while tl.sum((lo < hi).to(tl.int32), 0) > 0:
- mid = (lo + hi) // 2
- end = tl.load(
- query_start_loc + (mid + 1) * query_stride,
- lo < hi,
- other=0,
- )
- right = token >= end
- active = lo < hi
- lo = tl.where(active & right, mid + 1, lo)
- hi = tl.where(active & ~right, mid, hi)
- req = tl.minimum(lo, num_query_rows - 1).to(tl.int64)
- chunk_idx = tl.load(query_start_loc + req * query_stride)
- chunk_idx = tl.minimum(chunk_idx, num_tokens - 1).to(tl.int64)
- chunk_start = tl.load(positions + chunk_idx * position_stride)
- position = tl.load(positions + token * position_stride, valid, other=0).to(tl.int64)
- head = tl.arange(0, BLOCK_H)
- blocked = tl.full((BLOCK_T,), False, tl.int1)
- rolling = tl.full((BLOCK_T,), 0, tl.int64)
- for shift in tl.static_range(MAX_NGRAM):
- lookback = position - shift
- in_batch = lookback >= chunk_start
- batch_idx = tl.maximum(token - shift, 0)
- batch_token = tl.load(
- input_ids + batch_idx * input_stride, valid & in_batch, other=0
- )
- batch_source = tl.load(token_map + batch_token, valid & in_batch, other=0)
- batch_dead = tl.load(
- dead_mask + batch_idx * mask_stride, valid & in_batch, other=False
- )
- batch_source = tl.where(batch_dead, dead_id, batch_source)
- col = chunk_start - 1 - lookback
- in_window = valid & ~in_batch & (col >= 0) & (col < lookback_depth)
- col = tl.minimum(tl.maximum(col, 0), lookback_depth - 1)
- window_token = tl.load(
- lookback_token_ids + req * lookback_row_stride + col * lookback_col_stride,
- in_window,
- other=-1,
- )
- known = in_window & (window_token >= 0)
- window_source = tl.load(token_map + window_token, known, other=0)
- window_dead = tl.load(
- lookback_dead_mask
- + req * lookback_mask_row_stride
- + col * lookback_mask_col_stride,
- known,
- other=False,
- )
- window_source = tl.where(window_dead, dead_id, window_source)
- if cache is not None:
- clamped = tl.minimum(
- tl.maximum(lookback, 0), max_blocks * cache_block_size - 1
- )
- block_row = tl.minimum(req, num_table_rows - 1)
- needs_cache = valid & ~in_batch & ~known
- block = tl.load(
- block_table
- + block_row * table_stride
- + (clamped // cache_block_size) * table_col_stride,
- needs_cache,
- other=0,
- ).to(tl.int64)
- slot = tl.minimum(
- tl.maximum(block * cache_block_size + clamped % cache_block_size, 0),
- num_slots - 1,
- )
- fallback = tl.load(cache + slot, needs_cache, other=0)
- else:
- fallback = tl.full((BLOCK_T,), pad_id, tl.int32)
- source = tl.where(
- in_batch, batch_source, tl.where(known, window_source, fallback)
- ).to(tl.int64)
- blocked |= (lookback < 0) | (source == dead_id)
- value = tl.where(blocked, pad_id, source)
- multiplier = tl.load(multipliers + layer * MAX_NGRAM + shift)
- rolling ^= value * multiplier
- if shift > 0:
- col = (shift - 1) * num_heads + head
- param_offset = layer * (MAX_NGRAM - 1) * num_heads + col
- prime = tl.load(primes + param_offset, head < num_heads, other=1)
- offset = tl.load(offsets + param_offset, head < num_heads, other=0)
- hashed = rolling[:, None] % prime[None, :] + offset[None, :]
- out_offset = (token.to(tl.int64) * num_layers + layer)[:, None] * (
- (MAX_NGRAM - 1) * num_heads
- ) + col[None, :]
- tl.store(output + out_offset, hashed, valid[:, None] & (head < num_heads))
- class NgramHashState(nn.Module):
- """Maps each position to the hash ids of the n-grams ending there.
- Stateless on the V2 runner, which supplies every lookback token id. On
- the V1 runner it also keeps `hash_cache`, the slot-keyed rolling store
- of compressed ids (see module docstring), for generated tokens.
- """
- def __init__(
- self,
- vllm_config: VllmConfig,
- layout: EngramLayout,
- swa_cache_module: nn.Module,
- ) -> None:
- super().__init__()
- self.layout = layout
- self.swa_cache_module = swa_cache_module
- self.block_size: int = swa_cache_module.block_size
- self.lookback_depth: int = layout.max_ngram_size - 1
- self.use_slot_cache: bool = not vllm_config.use_v2_model_runner
- self._cache: torch.Tensor | None = None
- self._kv_cache_ref: weakref.ReferenceType[torch.Tensor] | None = None
- model_config = vllm_config.model_config
- from transformers import AutoTokenizer
- tokenizer = AutoTokenizer.from_pretrained(
- model_config.tokenizer,
- trust_remote_code=model_config.trust_remote_code,
- revision=model_config.revision,
- )
- token_map, vocab_size = build_compressed_token_map(tokenizer)
- if vocab_size != layout.compressed_vocab_size:
- raise ValueError(
- f"Compressed vocab size mismatch: built {vocab_size} from the "
- f"tokenizer, config expects {layout.compressed_vocab_size}; "
- "every hash multiplier derives from it, so the engram tables "
- "would be silently rehashed."
- )
- self.pad_id = token_map[layout.pad_token_id]
- multipliers = compute_hash_multipliers(
- layout.layer_ids, layout.max_ngram_size, vocab_size
- )
- self.register_buffer(
- "token_map", torch.tensor(token_map, dtype=torch.int32), persistent=False
- )
- self.register_buffer("primes", torch.tensor(layout.primes), persistent=False)
- self.register_buffer("offsets", layout.offsets, persistent=False)
- self.register_buffer("multipliers", multipliers, persistent=False)
- logger.info(
- "Built engram token map (%d -> %d ids) for layers %s",
- len(token_map),
- vocab_size,
- layout.layer_ids,
- )
- def ensure_cache(self) -> bool:
- """Lazily size the slot-keyed cache from the bound SWA KV cache.
- Returns False while the KV cache is unbound (profile run); the caller
- skips engram hashing then. Without the slot cache only that check
- remains.
- """
- kv_cache = self.swa_cache_module.kv_cache
- if kv_cache.numel() == 0:
- self._cache = None
- self._kv_cache_ref = None
- return False
- if not self.use_slot_cache:
- return True
- if self._kv_cache_ref is not None and self._kv_cache_ref() is kv_cache:
- return True
- # Graph memory profiling binds a temporary, smaller KV cache first.
- # Rebinding must discard its hash history without retaining KV storage.
- self._cache = torch.zeros(
- kv_cache.shape[0] * self.block_size,
- dtype=torch.int32,
- device=kv_cache.device,
- )
- self._kv_cache_ref = weakref.ref(kv_cache)
- return True
- def forward(
- self,
- input_ids: torch.Tensor,
- positions: torch.Tensor,
- query_start_loc: torch.Tensor,
- dead_mask: torch.Tensor,
- lookback_token_ids: torch.Tensor,
- lookback_dead_mask: torch.Tensor,
- slot_mapping: torch.Tensor | None,
- block_table: torch.Tensor | None,
- ) -> torch.Tensor:
- """Compute [tokens, layers, hash columns] int32 n-gram hashes.
- History comes from the current chunk, then the runner's lookback
- window, then the optional V1 slot cache. V2 needs only one launch.
- """
- cache = self._cache if self.use_slot_cache else None
- num_tokens = input_ids.shape[0]
- num_layers, max_ngram = self.multipliers.shape
- num_heads = self.primes.shape[-1]
- output = input_ids.new_empty(
- (num_tokens, num_layers, (max_ngram - 1) * num_heads), dtype=torch.int32
- )
- if num_tokens == 0:
- return output
- if self.use_slot_cache:
- assert cache is not None and slot_mapping is not None
- assert block_table is not None
- # Finish writes before other thread blocks read fallback history.
- _write_hash_cache_kernel[(triton.cdiv(num_tokens, 256),)](
- input_ids,
- self.token_map,
- dead_mask,
- slot_mapping,
- cache,
- num_tokens,
- input_ids.stride(0),
- dead_mask.stride(0),
- slot_mapping.stride(0),
- 256,
- DEAD_ID,
- )
- _hash_ids_kernel[(triton.cdiv(num_tokens, 32), num_layers)](
- input_ids,
- self.token_map,
- dead_mask,
- positions,
- block_table,
- query_start_loc,
- self.multipliers,
- self.primes,
- self.offsets,
- cache,
- lookback_token_ids,
- lookback_dead_mask,
- output,
- num_tokens,
- cache.shape[0] if cache is not None else 0,
- self.pad_id,
- input_stride=input_ids.stride(0),
- mask_stride=dead_mask.stride(0),
- position_stride=positions.stride(0),
- table_stride=block_table.stride(0) if block_table is not None else 0,
- table_col_stride=block_table.stride(1) if block_table is not None else 0,
- query_stride=query_start_loc.stride(0),
- num_query_rows=query_start_loc.numel() - 1,
- num_table_rows=block_table.shape[0] if block_table is not None else 0,
- max_blocks=block_table.shape[1] if block_table is not None else 0,
- cache_block_size=self.block_size,
- MAX_NGRAM=max_ngram,
- num_heads=num_heads,
- BLOCK_T=32,
- BLOCK_H=triton.next_power_of_2(num_heads),
- dead_id=DEAD_ID,
- lookback_depth=lookback_token_ids.shape[1],
- lookback_row_stride=lookback_token_ids.stride(0),
- lookback_col_stride=lookback_token_ids.stride(1),
- lookback_mask_row_stride=lookback_dead_mask.stride(0),
- lookback_mask_col_stride=lookback_dead_mask.stride(1),
- num_warps=4,
- )
- return output
- def _engram_head_shard_weight_loader(
- param: torch.nn.Parameter, loaded_weight: torch.Tensor
- ) -> None:
- """Load this rank's complete head buckets. ue8m0 scales arrive as
- float8_e8m0fnu; keep the raw bytes (the param stores uint8)."""
- part_rows = param.shape[0]
- if loaded_weight.dtype == torch.float8_e8m0fnu:
- loaded_weight = loaded_weight.view(torch.uint8)
- shard = loaded_weight.narrow(0, param.engram_vocab_start, part_rows)
- assert shard.shape == param.shape, (
- f"engram shard {tuple(shard.shape)} does not fit param {tuple(param.shape)}"
- )
- param.data.copy_(shard)
- @triton.jit
- def _engram_lookup_kernel(
- weight,
- scales,
- ids,
- out,
- vocab_start,
- vocab_end,
- num_rows,
- ids_stride_t,
- ids_stride_h,
- HEAD_START: tl.constexpr,
- LOCAL_HEADS: tl.constexpr,
- TOTAL_HEADS: tl.constexpr,
- DIM: tl.constexpr,
- QUANT_BLOCK: tl.constexpr,
- BLOCK_R: tl.constexpr,
- GRID: tl.constexpr,
- ):
- """Gather fp8 rows, apply their ue8m0 block scales, write bf16.
- Only this rank's heads are read; padded heads write zeros for all-gather.
- `weight`/`scales` may address pinned host memory through UVA.
- """
- cols = tl.arange(0, DIM)
- scale_cols = cols // QUANT_BLOCK
- for base in tl.range(tl.program_id(0) * BLOCK_R, num_rows, GRID * BLOCK_R):
- rows = base + tl.arange(0, BLOCK_R)
- valid = rows < num_rows
- head = HEAD_START + rows % LOCAL_HEADS
- token = (rows // LOCAL_HEADS).to(tl.int64)
- index = tl.load(
- ids + token * ids_stride_t + head * ids_stride_h,
- mask=valid & (head < TOTAL_HEADS),
- other=-1,
- ).to(tl.int64)
- owned = valid & (head < TOTAL_HEADS)
- owned &= (index >= vocab_start) & (index < vocab_end)
- local = tl.where(owned, index - vocab_start, 0)
- values = tl.load(
- weight + local[:, None] * DIM + cols[None, :],
- mask=owned[:, None],
- other=0.0,
- )
- scale = tl.load(
- scales + local[:, None] * (DIM // QUANT_BLOCK) + scale_cols[None, :],
- mask=owned[:, None],
- other=0,
- )
- # ue8m0 is a power of two, so its byte *is* the fp32 exponent field.
- scale = (scale.to(tl.int32) << 23).to(tl.float32, bitcast=True)
- tl.store(
- out + rows[:, None] * DIM + cols[None, :],
- (values.to(tl.float32) * scale).to(tl.bfloat16),
- mask=valid[:, None],
- )
- class ParallelEngramEmbedding(nn.Module):
- """The n-gram hash table, sharded by complete hash heads over TP ranks.
- Rows stay fp8 and are dequantized with ue8m0 per-32 scales on lookup.
- With `cpu_offload` the shard lives in pinned host memory and is read over
- UVA instead of HBM; the TP sharding is unchanged either way.
- """
- def __init__(
- self,
- num_embeddings: int,
- dim: int,
- head_sizes: tuple[int, ...],
- block_size: int = 32,
- cpu_offload: bool = False,
- ):
- super().__init__()
- tp_size = get_tensor_model_parallel_world_size()
- tp_rank = get_tensor_model_parallel_rank()
- assert head_sizes and all(size > 0 for size in head_sizes)
- assert sum(head_sizes) <= num_embeddings
- if cpu_offload and not is_uva_available():
- raise RuntimeError("Engram CPU offload requires UVA support")
- self.num_embeddings = num_embeddings
- self.dim = dim
- self.block_size = block_size
- self.n_hash_cols = len(head_sizes)
- self.part_n_hash_cols = triton.cdiv(self.n_hash_cols, tp_size)
- self.head_start = tp_rank * self.part_n_hash_cols
- head_end = self.head_start + self.part_n_hash_cols
- self.vocab_start_idx = sum(head_sizes[: self.head_start])
- self.vocab_end_idx = sum(head_sizes[:head_end])
- self.part_num_embeddings = self.vocab_end_idx - self.vocab_start_idx
- self.tp_size = tp_size
- self.cpu_offload = cpu_offload
- self._views: tuple[torch.Tensor, torch.Tensor] | None = None
- self._view_src: tuple[int, int] | None = None
- self._num_sms = torch.cuda.get_device_properties(
- torch.accelerator.current_device_index()
- ).multi_processor_count
- # Explicit device: model init runs under a `torch.device("cuda")`
- # context, which would otherwise put the shard in HBM.
- kwargs = {"device": "cpu", "pin_memory": True} if cpu_offload else {}
- self.weight = nn.Parameter(
- torch.empty(
- self.part_num_embeddings, dim, dtype=torch.float8_e4m3fn, **kwargs
- ),
- requires_grad=False,
- )
- self.weight_scale_inv = nn.Parameter(
- torch.empty(
- self.part_num_embeddings,
- dim // block_size,
- dtype=torch.uint8,
- **kwargs,
- ),
- requires_grad=False,
- )
- for param in (self.weight, self.weight_scale_inv):
- set_weight_attrs(
- param,
- {
- "weight_loader": _engram_head_shard_weight_loader,
- "engram_vocab_start": self.vocab_start_idx,
- },
- )
- if cpu_offload:
- logger.info(
- "Engram table offloaded to pinned host memory: %d rows x %d, "
- "%.2f GiB per rank",
- self.part_num_embeddings,
- dim,
- self.part_num_embeddings * (dim + dim // block_size) / 1024**3,
- )
- def _storage(self) -> tuple[torch.Tensor, torch.Tensor]:
- """Parameters when resident, else cached UVA views of the pinned shard.
- Rebuilt if anything swaps `.data`, so a stale device pointer cannot
- survive silently.
- """
- if not self.cpu_offload:
- return self.weight.data, self.weight_scale_inv.data
- src = (self.weight.data_ptr(), self.weight_scale_inv.data_ptr())
- if self._view_src != src:
- self._views = (
- get_accelerator_view_from_cpu_tensor(self.weight.data),
- get_accelerator_view_from_cpu_tensor(self.weight_scale_inv.data),
- )
- self._view_src = src
- assert self._views is not None
- return self._views
- def lookup(
- self, indices: torch.Tensor, out: torch.Tensor, background: bool = False
- ) -> None:
- """Look up local heads of [T, heads] into [T, local_heads, dim] bf16.
- `background` limits the grid to leave SMs for concurrent work.
- """
- rows = indices.shape[0] * self.part_n_hash_cols
- if not rows:
- return
- weight, scales = self._storage()
- # The table dwarfs TLB reach, so a persistent grid near the SM count
- # beats one program per row; halve it to leave SMs for the main stream.
- tiles = triton.cdiv(rows, 16)
- grid = min(tiles, self._num_sms // 2 if background else self._num_sms)
- _engram_lookup_kernel[(grid,)](
- weight,
- scales,
- indices,
- out,
- self.vocab_start_idx,
- self.vocab_end_idx,
- rows,
- indices.stride(0),
- indices.stride(1),
- HEAD_START=self.head_start,
- LOCAL_HEADS=self.part_n_hash_cols,
- TOTAL_HEADS=self.n_hash_cols,
- DIM=self.dim,
- QUANT_BLOCK=self.block_size,
- BLOCK_R=16,
- GRID=grid,
- )
- def forward(self, indices: torch.Tensor) -> torch.Tensor:
- """indices: [num_tokens, n_hash_cols] -> [num_tokens, n_hash_cols, dim]
- bf16, gathered from all TP shards."""
- out = torch.empty(
- (indices.shape[0], self.part_n_hash_cols, self.dim),
- dtype=torch.bfloat16,
- device=indices.device,
- )
- self.lookup(indices, out)
- if self.tp_size > 1:
- out = tensor_model_parallel_all_gather(out, dim=1)
- out = out[:, : self.n_hash_cols]
- return out
- @triton.jit(do_not_specialize=["num_kv_tokens"])
- def _fused_engram_post_wkv_kernel(
- hidden_states,
- kv,
- q_weight,
- k_weight,
- token_mask,
- output,
- num_kv_tokens,
- hidden_stride_t,
- hidden_stride_h,
- hidden_stride_d,
- kv_stride_t,
- kv_stride_d,
- q_stride_h,
- q_stride_d,
- k_stride_h,
- k_stride_d,
- mask_stride,
- output_stride_t,
- output_stride_h,
- output_stride_d,
- eps,
- clamp_value,
- DIM: tl.constexpr,
- HC_MULT: tl.constexpr,
- BLOCK_SIZE: tl.constexpr,
- HAS_MASK: tl.constexpr,
- ):
- program_idx = tl.program_id(0)
- token_idx = program_idx // HC_MULT
- hc_idx = program_idx % HC_MULT
- token_idx = token_idx.to(tl.int64)
- source_idx = token_idx
- source_valid = source_idx < num_kv_tokens
- dim_offsets = tl.arange(0, BLOCK_SIZE)
- dim_valid = dim_offsets < DIM
- hidden = tl.load(
- hidden_states
- + token_idx * hidden_stride_t
- + hc_idx * hidden_stride_h
- + dim_offsets * hidden_stride_d,
- mask=dim_valid,
- other=0.0,
- ).to(tl.float32)
- key = tl.load(
- kv + source_idx * kv_stride_t + (hc_idx * DIM + dim_offsets) * kv_stride_d,
- mask=source_valid & dim_valid,
- other=0.0,
- ).to(tl.float32)
- q = tl.load(
- q_weight + hc_idx * q_stride_h + dim_offsets * q_stride_d,
- mask=dim_valid,
- other=0.0,
- ).to(tl.float32)
- k = tl.load(
- k_weight + hc_idx * k_stride_h + dim_offsets * k_stride_d,
- mask=dim_valid,
- other=0.0,
- ).to(tl.float32)
- hidden_rms = tl.rsqrt(tl.sum(hidden * hidden, axis=0) / DIM + eps)
- key_rms = tl.rsqrt(tl.sum(key * key, axis=0) / DIM + eps)
- dot = tl.sum(hidden * q * k * key, axis=0)
- dot *= hidden_rms * key_rms * tl.rsqrt(DIM * 1.0)
- gate_input = tl.sqrt(tl.maximum(tl.abs(dot), clamp_value))
- gate_input = tl.where(dot < 0.0, -gate_input, gate_input)
- gate = tl.sigmoid(gate_input)
- if HAS_MASK:
- active = tl.load(
- token_mask + source_idx * mask_stride,
- mask=source_valid,
- other=0,
- )
- gate = tl.where(active, gate, 0.0)
- value = tl.load(
- kv + source_idx * kv_stride_t + (HC_MULT * DIM + dim_offsets) * kv_stride_d,
- mask=source_valid & dim_valid,
- other=0.0,
- ).to(tl.float32)
- tl.store(
- output
- + token_idx * output_stride_t
- + hc_idx * output_stride_h
- + dim_offsets * output_stride_d,
- hidden + gate * value,
- mask=dim_valid,
- )
- @triton.jit(do_not_specialize=["num_tokens", "token_start", "num_elements"])
- def _engram_sp_rows_kernel(
- gathered,
- output,
- num_tokens,
- token_start,
- num_elements,
- LOCAL_WIDTH: tl.constexpr,
- WIDTH: tl.constexpr,
- BLOCK_SIZE: tl.constexpr,
- ):
- offsets = tl.program_id(0).to(tl.int64) * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE)
- tokens = token_start + offsets // WIDTH
- cols = offsets % WIDTH
- source = (cols // LOCAL_WIDTH * num_tokens + tokens) * LOCAL_WIDTH
- source += cols % LOCAL_WIDTH
- values = tl.load(
- gathered + source, (offsets < num_elements) & (tokens < num_tokens), other=0
- )
- tl.store(output + offsets, values, offsets < num_elements)
- class Engram(nn.Module):
- """Writes an n-gram lookup into the residual stream, gated by how well it
- matches that stream.
- The hash ids fetch `n_hash_cols` rows; `wkv` turns them into one key per
- hc copy plus a shared value. The gate is a normalized dot product of the
- stream against the key, signed-sqrt'ed before the sigmoid (matching the
- training kernel).
- """
- def __init__(
- self,
- config,
- quant_config: QuantizationConfig | None,
- layout: EngramLayout,
- layer_hash_index: int,
- use_sequence_parallel: bool,
- prefix: str,
- ) -> None:
- super().__init__()
- self.layer_hash_index = layer_hash_index
- self.dim = config.hidden_size
- self.hc_mult = config.hc_mult
- self.eps = config.rms_norm_eps
- self.clamp_value = 1e-6
- self.use_sequence_parallel = use_sequence_parallel
- # Named ``embed_tokens`` so the checkpoint's ``engram.embed.weight``
- # survives the mapper's ``embed.weight`` -> ``embed_tokens.weight``
- # suffix rule.
- engram_config = get_current_vllm_config().engram_config
- self.embed_tokens = ParallelEngramEmbedding(
- layout.num_embeddings[layer_hash_index],
- layout.head_dim,
- tuple(size for order in layout.primes[layer_hash_index] for size in order),
- cpu_offload=engram_config.cpu_offload if engram_config else True,
- )
- n_hash_cols = (layout.max_ngram_size - 1) * layout.n_heads
- self.wkv = ReplicatedLinear(
- n_hash_cols * layout.head_dim,
- self.dim * (self.hc_mult + 1),
- bias=False,
- quant_config=quant_config,
- return_bias=False,
- prefix=f"{prefix}.wkv",
- )
- self.q_weight = nn.Parameter(
- torch.empty(self.hc_mult, self.dim, dtype=torch.bfloat16),
- requires_grad=False,
- )
- self.k_weight = nn.Parameter(
- torch.empty(self.hc_mult, self.dim, dtype=torch.bfloat16),
- requires_grad=False,
- )
- max_tokens = get_current_vllm_config().scheduler_config.max_num_batched_tokens
- # Keep lookup results alive across breakable graph segments.
- self.staged_rows = torch.empty(
- max_tokens,
- self.embed_tokens.part_n_hash_cols,
- layout.head_dim,
- dtype=torch.bfloat16,
- )
- def prepare_embeddings(self, hash_ids: torch.Tensor) -> None:
- """Gather this layer's rows on the main stream before decoder layers."""
- self.embed_tokens.lookup(hash_ids, self.staged_rows[: hash_ids.shape[0]])
- def embed(self, hash_ids: torch.Tensor) -> torch.Tensor:
- """Gather heads, returning only local tokens when SP is enabled."""
- rows = self.staged_rows[: hash_ids.shape[0]]
- if self.embed_tokens.tp_size == 1:
- return rows
- if self.use_sequence_parallel:
- tp_size = self.embed_tokens.tp_size
- num_tokens, local_heads, dim = rows.shape
- gathered = tensor_model_parallel_all_gather(rows, dim=0)
- chunk = (num_tokens + tp_size - 1) // tp_size
- rows = rows.new_empty((chunk, self.embed_tokens.n_hash_cols, dim))
- _engram_sp_rows_kernel[(triton.cdiv(rows.numel(), 1024),)](
- gathered,
- rows,
- num_tokens,
- get_tensor_model_parallel_rank() * chunk,
- rows.numel(),
- local_heads * dim,
- self.embed_tokens.n_hash_cols * dim,
- BLOCK_SIZE=1024,
- )
- return rows
- rows = tensor_model_parallel_all_gather(rows, dim=1)
- return rows[:, : self.embed_tokens.n_hash_cols]
- def forward(
- self,
- hidden_states: torch.Tensor,
- hash_ids: torch.Tensor,
- token_mask: torch.Tensor | None = None,
- ) -> torch.Tensor:
- """hidden_states: [T, hc_mult, dim]; hash_ids: [T, n_hash_cols] (all
- tokens, pre sequence-parallel shard); token_mask: [T], False shuts
- the gate so those positions pass through untouched."""
- kv = self.wkv(self.embed(hash_ids).flatten(-2))
- num_kv_tokens = hash_ids.shape[0]
- assert token_mask is None or token_mask.shape == (num_kv_tokens,)
- if self.use_sequence_parallel:
- tp_size = get_tensor_model_parallel_world_size()
- tp_rank = get_tensor_model_parallel_rank()
- shard_size = (num_kv_tokens + tp_size - 1) // tp_size
- assert hidden_states.shape[0] == shard_size
- start = min(tp_rank * shard_size, num_kv_tokens)
- num_kv_tokens = min(shard_size, num_kv_tokens - start)
- if token_mask is not None:
- token_mask = token_mask[start : start + num_kv_tokens]
- num_tokens, hc_mult, dim = hidden_states.shape
- assert hc_mult == self.hc_mult and dim == self.dim
- assert kv.ndim == 2 and kv.shape[1] == (hc_mult + 1) * dim
- output = torch.empty_like(hidden_states)
- if num_tokens == 0:
- return output
- block_size = triton.next_power_of_2(dim)
- num_warps = 8 if block_size >= 2048 else 4
- mask = token_mask if token_mask is not None else hidden_states
- _fused_engram_post_wkv_kernel[(num_tokens * hc_mult,)](
- hidden_states,
- kv,
- self.q_weight,
- self.k_weight,
- mask,
- output,
- num_kv_tokens,
- hidden_states.stride(0),
- hidden_states.stride(1),
- hidden_states.stride(2),
- kv.stride(0),
- kv.stride(1),
- self.q_weight.stride(0),
- self.q_weight.stride(1),
- self.k_weight.stride(0),
- self.k_weight.stride(1),
- token_mask.stride(0) if token_mask is not None else 0,
- output.stride(0),
- output.stride(1),
- output.stride(2),
- self.eps,
- self.clamp_value,
- DIM=dim,
- HC_MULT=hc_mult,
- BLOCK_SIZE=block_size,
- HAS_MASK=token_mask is not None,
- num_warps=num_warps,
- )
- return output
|