File size: 8,869 Bytes
c6405fd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
"""
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

# ============================================================
# 1. 前缀缓存池: 多个会话共享 System Prompt 的 KV
# ============================================================

@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}


# ============================================================
# 2. 会话层: 每 Agent 一个, 引用共享前缀 + 维护增量 KV
# ============================================================

@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,
        }


# ============================================================
# 3. KV 编码器: 吸收式 MLA + 分层量化
# ============================================================

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)


# ============================================================
# 4. 容量估算
# ============================================================

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")


# ============================================================
# 5. 演示
# ============================================================

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)

    # Agent A: 思考(INT8) + 工具结果(INT4)
    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))
    # Agent B: 只思考(INT8)
    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 核心逻辑验证完成")