| """ |
| AgentFrame 核心: 前缀感知缓存池 (Prefix-Aware Cache Pool) |
| ======================================================= |
| 专为 Agent 场景设计的 KV 缓存复用框架 |
| |
| 核心思想: |
| 1. Agent 的 System Prompt + 工具定义是稳定前缀 (几千 token) |
| 2. 多个 Agent 会话共享同一前缀的 KV 缓存 (只存 1 份) |
| 3. 增量部分按信息价值分层量化 (思考链 INT8, 工具结果 INT4) |
| 4. 基于吸收式 MLA 缓存 (270KB → 7.6KB/token) |
| |
| 逻辑验证版 (numpy), 架构与 torch 版一致 |
| """ |
| import hashlib |
| import threading |
| from dataclasses import dataclass, field |
| from typing import Dict, Optional, List, Tuple |
| import numpy as np |
|
|
| |
| |
| |
|
|
| @dataclass |
| class PrefixCache: |
| """一个 Agent System Prompt 的共享 KV 缓存""" |
| prefix_text: str |
| prefix_hash: str |
| num_tokens: int |
| layers: Dict[int, Dict] = field(default_factory=dict) |
| ref_count: int = 0 |
| lock: threading.Lock = field(default_factory=threading.Lock) |
|
|
|
|
| class PrefixPool: |
| """前缀缓存池: 按 System Prompt 内容去重, 只存一份 KV""" |
|
|
| def __init__(self): |
| self._pool: Dict[str, PrefixCache] = {} |
| self._lock = threading.Lock() |
|
|
| @staticmethod |
| def _hash(text: str) -> str: |
| return hashlib.sha256(text.encode()).hexdigest()[:16] |
|
|
| def acquire(self, system_prompt: str, num_tokens: int) -> PrefixCache: |
| h = self._hash(system_prompt) |
| with self._lock: |
| if h in self._pool: |
| cache = self._pool[h] |
| cache.ref_count += 1 |
| return cache |
| cache = PrefixCache( |
| prefix_text=system_prompt, |
| prefix_hash=h, |
| num_tokens=num_tokens, |
| ) |
| cache.ref_count = 1 |
| self._pool[h] = cache |
| return cache |
|
|
| def release(self, cache: PrefixCache): |
| with self._lock: |
| cache.ref_count -= 1 |
| if cache.ref_count <= 0: |
| self._pool.pop(cache.prefix_hash, None) |
| print(f"🗑 前缀缓存已淘汰: {cache.prefix_hash}") |
|
|
| def memory_usage(self) -> Dict: |
| """当前池内存占用 (字节)""" |
| total = 0 |
| for h, c in self._pool.items(): |
| for layer_data in c.layers.values(): |
| for k, v in layer_data.items(): |
| if isinstance(v, np.ndarray): |
| total += v.size * v.itemsize |
| return {"cached_prefixes": len(self._pool), "bytes": total, "MB": total / 1024**2} |
|
|
|
|
| |
| |
| |
|
|
| @dataclass |
| class AgentSession: |
| session_id: str |
| prefix: PrefixCache |
| incremental_layers: Dict[int, List] = field(default_factory=dict) |
| thinking_bits: int = 8 |
| toolresult_bits: int = 4 |
|
|
|
|
| class SessionManager: |
| def __init__(self, pool: PrefixPool): |
| self.pool = pool |
| self.sessions: Dict[str, AgentSession] = {} |
| self._lock = threading.Lock() |
|
|
| def create_session(self, session_id: str, system_prompt: str, prompt_tokens: int) -> AgentSession: |
| prefix = self.pool.acquire(system_prompt, prompt_tokens) |
| session = AgentSession(session_id=session_id, prefix=prefix) |
| with self._lock: |
| self.sessions[session_id] = session |
| return session |
|
|
| def close_session(self, session_id: str): |
| with self._lock: |
| session = self.sessions.pop(session_id, None) |
| if session: |
| self.pool.release(session.prefix) |
|
|
| def append_tool_result(self, session: AgentSession, layer_idx: int, tensor: np.ndarray): |
| session.incremental_layers.setdefault(layer_idx, []).append(tensor) |
|
|
| def session_memory(self, session: AgentSession) -> Dict: |
| inc_bytes = 0 |
| for layer_idx, tensors in session.incremental_layers.items(): |
| for t in tensors: |
| inc_bytes += t.size * t.itemsize |
| prefix_bytes = self.pool.memory_usage()["bytes"] / max(1, session.prefix.ref_count) |
| return { |
| "prefix_shared_bytes": prefix_bytes, |
| "incremental_bytes": inc_bytes, |
| "total_bytes": prefix_bytes + inc_bytes, |
| "prefix_refs": session.prefix.ref_count, |
| } |
|
|
|
|
| |
| |
| |
|
|
| class AbsorbedMLAEncoder: |
| def __init__(self, kv_lora_rank=512, qk_rope=64, n_layers=27): |
| self.kv_rank = kv_lora_rank |
| self.k_rope = qk_rope |
| self.n_layers = n_layers |
|
|
| def _quantize(self, tensor: np.ndarray, bits: int, n_ch: int = 32) -> np.ndarray: |
| """per-channel 非对称量化""" |
| d = tensor.shape[-1] |
| ch = d // n_ch |
| tc = tensor.reshape(-1, n_ch, ch) |
| tmin = tc.min(axis=-1, keepdims=True) |
| tmax = tc.max(axis=-1, keepdims=True) |
| scale = (tmax - tmin) / (2**bits - 1) |
| q = np.round((tc - tmin) / (scale + 1e-8)) |
| q = np.clip(q, 0, 2**bits - 1) |
| return (q * scale + tmin).reshape(tensor.shape) |
|
|
| def encode_thought(self, kv: np.ndarray) -> np.ndarray: |
| """思考链区: INT8 (误差 0.011)""" |
| return self._quantize(kv, bits=8) |
|
|
| def encode_tool_result(self, kv: np.ndarray) -> np.ndarray: |
| """工具结果区: INT4 (误差 0.079)""" |
| return self._quantize(kv, bits=4) |
|
|
| def per_token_bytes(self, bits: int) -> float: |
| """每 token 每层 KV 大小 (字节)""" |
| return (self.kv_rank * bits / 8) + (self.k_rope * bits / 8) |
|
|
|
|
| |
| |
| |
|
|
| def capacity_estimate(encoder: AbsorbedMLAEncoder, vram_gb: float = 10.0): |
| tok4 = encoder.per_token_bytes(4) |
| total_bytes = vram_gb * 1024**3 |
| prefix_tokens = 3000 |
| inc_tokens = 5000 |
|
|
| print(f"\n{'='*60}") |
| print(f"AgentFrame 容量估算 (L40S 10GB KV 空间)") |
| print(f"{'='*60}") |
| print(f"Agent 典型: 前缀 3000 token (共享) + 增量 5000 token/会话") |
| print(f"每 token KV: INT4 = {tok4:.1f}B/层, 27层 = {tok4*27/1024:.1f}KB") |
|
|
| print(f"\n📊 AgentFrame (前缀共享 + INT4):") |
| for n_agents in [1, 5, 10, 30, 50]: |
| prefix_cost = prefix_tokens * tok4 * encoder.n_layers |
| inc_cost = inc_tokens * tok4 * encoder.n_layers * n_agents |
| total = prefix_cost + inc_cost |
| fits = total <= total_bytes |
| print(f" {n_agents:>2} 个 Agent: {'✅' if fits else '❌'} {total/1024**2:.0f}MB / {vram_gb}GB") |
|
|
| print(f"\n📊 普通框架 (270KB/token, 无共享):") |
| for n_agents in [1, 2, 3, 5]: |
| cost = (prefix_tokens + inc_tokens) * 276480 * n_agents |
| fits = cost <= total_bytes |
| print(f" {n_agents:>2} 个 Agent: {'✅' if fits else '❌'} {cost/1024**2:.0f}MB / {vram_gb}GB") |
|
|
|
|
| |
| |
| |
|
|
| if __name__ == "__main__": |
| print("=" * 60) |
| print("AgentFrame: 前缀感知缓存池 演示") |
| print("=" * 60) |
|
|
| pool = PrefixPool() |
| sessions = SessionManager(pool) |
|
|
| sys_prompt = """你是智能助手。你有以下工具可用: |
| - search(query): 搜索网络 |
| - calculator(expr): 数学计算 |
| - code_runner(code): 执行代码 |
| 请根据用户需求选择合适的工具。""" |
| prompt_tokens = 3000 |
|
|
| agent_a = sessions.create_session("agent-A", sys_prompt, prompt_tokens) |
| agent_b = sessions.create_session("agent-B", sys_prompt, prompt_tokens) |
|
|
| print(f"\n✅ Agent A + B 共享前缀: ref_count = {agent_a.prefix.ref_count}") |
|
|
| encoder = AbsorbedMLAEncoder() |
| fake_kv = np.random.randn(1, 64, encoder.kv_rank) |
|
|
| |
| sessions.append_tool_result(agent_a, 0, encoder.encode_thought(fake_kv)) |
| sessions.append_tool_result(agent_a, 0, encoder.encode_tool_result(fake_kv)) |
| |
| sessions.append_tool_result(agent_b, 0, encoder.encode_thought(fake_kv)) |
|
|
| print(f"\n📊 前缀池状态: {pool.memory_usage()}") |
| print(f"📊 Agent A 内存: {sessions.session_memory(agent_a)}") |
| print(f"📊 Agent B 内存: {sessions.session_memory(agent_b)}") |
|
|
| capacity_estimate(encoder) |
|
|
| sessions.close_session("agent-A") |
| sessions.close_session("agent-B") |
| print(f"\n✅ 会话关闭后前缀池: {pool.memory_usage()}") |
| print("\n✅ AgentFrame 核心逻辑验证完成") |
|
|