Download source/src/vimeml/deployment/kv_cache.py from Voltline/vimeml-tiny-ja-v4.2: direct link, hf CLI and curl.
- Browser
- Download file 11 kB
-
https://huggingface.co/Voltline/vimeml-tiny-ja-v4.2/resolve/main/source/src/vimeml/deployment/kv_cache.py
- Command line
-
hf download hf://Voltline/vimeml-tiny-ja-v4.2/source/src/vimeml/deployment/kv_cache.py
-
curl -L -o kv_cache.py https://huggingface.co/Voltline/vimeml-tiny-ja-v4.2/resolve/main/source/src/vimeml/deployment/kv_cache.py
11 kB
| """V2 incremental attention with explicit, immutable FP32 cache snapshots. | |
| Caches belong to one request. Reuse only exact token prefixes, with absolute | |
| learned positions; never slide, persist across requests, or mutate a branch. | |
| """ | |
| import math | |
| from pathlib import Path | |
| import numpy as np | |
| import torch | |
| from torch import nn | |
| from vimeml.deployment import v2 | |
| from vimeml.deployment.bundle import fresh_directory, read_json, write_json | |
| from vimeml.deployment.coreml import coremltools, inspect_spec | |
| from vimeml.training.data import file_sha | |
| FORMAT = "vimeml_coreml_v2_kv_v1" | |
| class CacheGraph(nn.Module): | |
| def __init__(self, model): | |
| super().__init__() | |
| self.model = model.eval() | |
| c = model.config | |
| self.width, self.heads, self.head_dim = c.d_model, c.n_heads, c.d_model // c.n_heads | |
| mask = torch.full((c.context_length, c.context_length), -float("inf")) | |
| self.register_buffer("causal_mask", torch.triu(mask, diagonal=1)) | |
| def forward(self, input_ids, cache_length, key_cache, value_cache): | |
| length = input_ids.shape[1] | |
| past = cache_length[0] | |
| positions = torch.arange(length, device=input_ids.device, dtype=torch.int32) + past | |
| hidden = self.model.token_embedding(input_ids) + self.model.position_embedding(positions) | |
| # Rectangular mask: queries have absolute positions past..end-1. | |
| mask = self.causal_mask[positions.long()] | |
| keys, values = [], [] | |
| for i, block in enumerate(self.model.blocks): | |
| normalized = block.attention_norm(hidden) | |
| qkv = block.attention.qkv(normalized).reshape(1, length, 3, self.heads, self.head_dim) | |
| query, key, value = qkv.permute(2, 0, 3, 1, 4).unbind(0) | |
| indices = (positions.long(),) | |
| key = key_cache[i].permute(2, 0, 1, 3).index_put(indices, key.permute(2, 0, 1, 3)).permute(1, 2, 0, 3) | |
| value = value_cache[i].permute(2, 0, 1, 3).index_put(indices, value.permute(2, 0, 1, 3)).permute(1, 2, 0, 3) | |
| attention = torch.softmax(torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(self.head_dim) + mask, dim=-1) | |
| attended = torch.matmul(attention, value).transpose(1, 2).reshape(1, length, self.width) | |
| hidden = hidden + block.attention.projection(attended) | |
| hidden = hidden + block.ffn(block.ffn_norm(hidden)) | |
| keys.append(key) | |
| values.append(value) | |
| # Output exactly the new chunk. A one-token decode emits one row; | |
| # avoid a second dynamic output dimension from runtime logit slicing. | |
| logits = self.model.lm_head(self.model.final_norm(hidden)) | |
| return logits, torch.stack(keys), torch.stack(values) | |
| def cache_shape(config): | |
| return (config.n_layers, 1, config.n_heads, config.context_length, config.d_model // config.n_heads) | |
| def empty_cache(config): | |
| return tuple(np.zeros(cache_shape(config), dtype=np.float32) for _ in range(2)) | |
| def trace(model): | |
| graph = CacheGraph(model).eval() | |
| keys, values = (torch.from_numpy(a) for a in empty_cache(model.config)) | |
| traced = torch.jit.trace(graph, (torch.tensor([[2, 4, 7]], dtype=torch.int32), | |
| torch.tensor([0], dtype=torch.int32), keys, values)) | |
| # Exercise a nonzero past, single-token decode and the full capacity, | |
| # rather than checking only the trace example. | |
| generator = torch.Generator().manual_seed(716) | |
| ids = torch.randint(model.config.vocab_size, (1, model.config.context_length), generator=generator, dtype=torch.int32) | |
| expected = model(ids) | |
| for fn in (graph, traced): | |
| state = (keys, values) | |
| start = 0 | |
| for size in (1, 3, model.config.context_length - 4): | |
| actual, k, v = fn(ids[:, start:start+size], torch.tensor([start], dtype=torch.int32), | |
| *state) | |
| torch.testing.assert_close(actual, expected[:, start:start+size], atol=3e-5, rtol=3e-4) | |
| state = k, v | |
| start += size | |
| actual, _, _ = fn(ids, torch.tensor([0], dtype=torch.int32), keys, values) | |
| torch.testing.assert_close(actual, expected, atol=3e-5, rtol=3e-4) | |
| return traced | |
| def finish(output, model, metadata): | |
| fresh_directory(output) | |
| model.save(str(Path(output) / "model.mlpackage")) | |
| files = v2.inventory(Path(output) / "model.mlpackage") | |
| write_json(Path(output) / "manifest.json", {"format": FORMAT, "status": "complete", | |
| "architecture": v2.ARCHITECTURE, **metadata, "environment": v2.environment(), | |
| "spec": inspect_spec(model), "package_files": files, | |
| "package_bytes": sum(item["bytes"] for item in files.values())}) | |
| def verify_package(directory): | |
| directory = Path(directory) | |
| m = read_json(directory / "manifest.json") | |
| if (m.get("format") != FORMAT or m.get("status") != "complete" | |
| or m.get("architecture") != v2.ARCHITECTURE or v2.inventory(directory / "model.mlpackage") != m["package_files"]): | |
| raise ValueError("Invalid KV package or changed file inventory.") | |
| return m | |
| def convert(bundle, output): | |
| if Path(output).exists(): | |
| raise ValueError("Output exists; choose a new experiment.") | |
| lm, ct = v2.BundleLM(bundle), coremltools() | |
| c = lm.model.config | |
| result = ct.convert(trace(lm.model), source="pytorch", convert_to="mlprogram", | |
| minimum_deployment_target=ct.target.iOS18, compute_precision=ct.precision.FLOAT32, | |
| inputs=[ct.TensorType(name="input_ids", shape=(1, ct.RangeDim(1, c.context_length, default=1)), dtype=np.int32), | |
| ct.TensorType(name="cache_length", shape=(1,), dtype=np.int32), | |
| ct.TensorType(name="key_cache", shape=cache_shape(c), dtype=np.float32), | |
| ct.TensorType(name="value_cache", shape=cache_shape(c), dtype=np.float32)], | |
| outputs=[ct.TensorType(name="logits", dtype=np.float32), ct.TensorType(name="new_key_cache", dtype=np.float32), | |
| ct.TensorType(name="new_value_cache", dtype=np.float32)], skip_model_load=True) | |
| finish(output, result, {"kind": "fp32", "minimum_ios": 18, "bundle": lm.metadata, | |
| "cache_shape": cache_shape(c), "cache_dtype": "float32", "compute_units": "CPU_ONLY", | |
| "cache_bytes_per_snapshot": 2 * int(np.prod(cache_shape(c))) * 4, | |
| "interface": "batch1; absolute learned positions; 0<=past, 1<=Q, past+Q<=128; logits [1,Q,V] for new tokens only. Explicit immutable K/V snapshots.", | |
| "conversion_source_sha256": file_sha(Path(__file__)), | |
| "policy": "No training/weight changes. Request-local exact token prefix reuse; no sliding or cross-request retention."}) | |
| def compress(source, output, alignment): | |
| if Path(output).exists(): | |
| raise ValueError("Output exists; choose a new experiment.") | |
| m, gate = verify_package(source), read_json(alignment) | |
| if (m["kind"] != "fp32" or not gate.get("passed") or gate.get("format") != "vimeml_kv_alignment_v1" | |
| or gate.get("coreml_manifest_sha256") != file_sha(Path(source) / "manifest.json")): | |
| raise ValueError("A passed alignment report for this exact FP32 KV package is required.") | |
| ct = coremltools() | |
| from coremltools.optimize import coreml as opt | |
| model = ct.models.MLModel(str(Path(source) / "model.mlpackage"), skip_model_load=True) | |
| config = opt.OpLinearQuantizerConfig(mode="linear_symmetric", dtype="int8", granularity="per_block", block_size=32, weight_threshold=2048) | |
| optimization, selection = v2.parameter_selection(opt, model, config) | |
| result = opt.linear_quantize_weights(model, config=optimization) | |
| finish(output, result, {key: m[key] for key in ("minimum_ios", "bundle", "cache_shape", "cache_dtype", "compute_units", "cache_bytes_per_snapshot", "interface", "policy")} | { | |
| "kind": "linear8_fp32_compute", "source_manifest_sha256": file_sha(Path(source) / "manifest.json"), | |
| "fp32_alignment_sha256": file_sha(alignment), "compression": {"method": "linear_symmetric", "bits": 8, | |
| "block_size": 32, "selection": selection}, "conversion_source_sha256": file_sha(Path(__file__))}) | |
| class Runtime: | |
| def __init__(self, bundle, directory): | |
| lm = v2.BundleLM(bundle) | |
| self.config, self.metadata = lm.model.config, dict(lm.metadata) | |
| self.manifest = verify_package(directory) | |
| if self.manifest["bundle"]["bundle_manifest_sha256"] != self.metadata["bundle_manifest_sha256"]: | |
| raise ValueError("KV model belongs to another bundle.") | |
| if (self.manifest["cache_shape"] != list(cache_shape(self.config)) | |
| or self.manifest["cache_dtype"] != "float32" or self.manifest["compute_units"] != "CPU_ONLY"): | |
| raise ValueError("KV package cache/compute contract differs.") | |
| ct = coremltools() | |
| self.model = ct.models.MLModel(str(Path(directory) / "model.mlpackage"), compute_units=ct.ComputeUnit.CPU_ONLY) | |
| self.metadata.update(precision=self.manifest["kind"], compute_units="CPU_ONLY", | |
| coreml_manifest_sha256=file_sha(Path(directory) / "manifest.json")) | |
| self.calls = self.processed_tokens = 0 | |
| def step(self, ids, state=None, past=0, last_only=False): | |
| raw = np.asarray(ids) | |
| if (not raw.size or not np.issubdtype(raw.dtype, np.integer) | |
| or raw.ndim not in (1, 2) or (raw.ndim == 2 and raw.shape[0] != 1) | |
| or (raw < 0).any() or (raw >= self.config.vocab_size).any()): | |
| raise ValueError("Invalid incremental token tensor.") | |
| ids = raw.astype(np.int32).reshape(1, -1) | |
| if (not isinstance(past, int) or past < 0 or past + ids.shape[1] > self.config.context_length | |
| or (state is None and past != 0)): | |
| raise ValueError("Invalid incremental input/cache position.") | |
| state = empty_cache(self.config) if state is None else state | |
| if len(state) != 2 or any(not isinstance(a, np.ndarray) or a.shape != cache_shape(self.config) | |
| or a.dtype != np.float32 or not np.isfinite(a).all() for a in state): | |
| raise ValueError("Invalid cache tensor.") | |
| result = self.model.predict({"input_ids": ids, "cache_length": np.array([past], np.int32), | |
| "key_cache": state[0], "value_cache": state[1]}) | |
| self.calls += 1 | |
| self.processed_tokens += ids.size | |
| # Own prediction output memory, as in the existing stateless adapter. | |
| # A retained branch snapshot must survive subsequent runtime calls. | |
| logits = np.array(result["logits"], dtype=np.float32, copy=True) | |
| caches = tuple(np.array(result[name], dtype=np.float32, copy=True) | |
| for name in ("new_key_cache", "new_value_cache")) | |
| if (logits.shape != (1, ids.size, self.config.vocab_size) | |
| or not np.isfinite(logits).all() | |
| or any(a.shape != cache_shape(self.config) or not np.isfinite(a).all() for a in caches)): | |
| raise ValueError("Invalid Core ML cache/logits output.") | |
| return (logits[:, -1:].copy() if last_only else logits), caches | |