import argparse
import datetime
import json
import os
import struct
import sys
import time
from pathlib import Path
import torch
from datasets import load_dataset
from transformers import AutoModelForCausalLM, AutoTokenizer
from transformers.cache_utils import DynamicCache, DynamicLayer
def per_token_nll(model, input_ids, attention_mask, eval_start):
out = model(input_ids=input_ids, attention_mask=attention_mask, use_cache=False)
logits = out.logits shift_logits = logits[..., eval_start - 1 : -1, :].contiguous()
shift_labels = input_ids[..., eval_start:].contiguous()
nll_total = 0.0
n_tokens = 0
chunk = 64
for i in range(0, shift_labels.size(1), chunk):
sl = shift_logits[:, i : i + chunk, :].float()
st = shift_labels[:, i : i + chunk]
log_probs = torch.nn.functional.log_softmax(sl, dim=-1)
nll = -log_probs.gather(2, st.unsqueeze(-1)).squeeze(-1)
nll_total += nll.sum().item()
n_tokens += st.numel()
return nll_total / n_tokens, n_tokens
def load_kv_binary(path):
with open(path, "rb") as f:
manifest_len = struct.unpack("<Q", f.read(8))[0]
manifest = json.loads(f.read(manifest_len).decode("utf-8"))
layers = []
for _ in range(manifest["num_layers"]):
k_len = struct.unpack("<I", f.read(4))[0]
k_bytes = f.read(k_len * 4)
k = struct.unpack(f"<{k_len}f", k_bytes)
v_len = struct.unpack("<I", f.read(4))[0]
v_bytes = f.read(v_len * 4)
v = struct.unpack(f"<{v_len}f", v_bytes)
layers.append((k, v))
return manifest, layers
def build_cache_from_kv(layers, num_tokens, num_kv_heads, head_dim, device, dtype):
cache = DynamicCache()
cache.layers = [DynamicLayer() for _ in range(len(layers))]
for i, (k_flat, v_flat) in enumerate(layers):
k = torch.tensor(k_flat, dtype=dtype, device=device).reshape(
1, num_tokens, num_kv_heads, head_dim
).transpose(1, 2) v = torch.tensor(v_flat, dtype=dtype, device=device).reshape(
1, num_tokens, num_kv_heads, head_dim
).transpose(1, 2)
cache.layers[i].keys = k
cache.layers[i].values = v
return cache
def main():
p = argparse.ArgumentParser()
p.add_argument("--model", default="Qwen/Qwen2.5-0.5B-Instruct")
p.add_argument("--model-slug", default="qwen2.5-0.5b")
p.add_argument("--corpus", default="wikitext-2")
p.add_argument("--n-tokens", type=int, default=1024)
p.add_argument("--shared-frac", type=float, default=0.8)
p.add_argument("--ppl-frac", type=float, default=0.3)
p.add_argument("--device", default="cuda")
p.add_argument("--multi-agent-dir", type=Path, required=True)
p.add_argument("--output", type=Path, required=True)
args = p.parse_args()
print(f"[multi-agent] model={args.model} corpus={args.corpus} n_tokens={args.n_tokens}")
print(f"[multi-agent] shared_frac={args.shared_frac} multi_agent_dir={args.multi_agent_dir}")
if args.corpus == "wikitext-2":
ds = load_dataset("Salesforce/wikitext", "wikitext-2-raw-v1", split="test")
text = "\n\n".join(ds["text"])
elif args.corpus.startswith("file:"):
text = Path(args.corpus[5:]).read_text()
else:
raise ValueError(f"unsupported corpus: {args.corpus}")
tokenizer = AutoTokenizer.from_pretrained(args.model)
enc = tokenizer(text, return_tensors="pt", add_special_tokens=False)
input_ids = enc.input_ids[:, : args.n_tokens]
n_shared = int(args.n_tokens * args.shared_frac)
print(f"[multi-agent] input_ids shape: {tuple(input_ids.shape)}; n_shared={n_shared}")
with open(args.multi_agent_dir / "shared_pool_receipt.json") as f:
shared_pool_receipt = json.load(f)
with open(args.multi_agent_dir / "agents_receipt.json") as f:
agent_receipts = json.load(f)
n_agents = len(agent_receipts)
print(f"[multi-agent] shared pool: {shared_pool_receipt['pool_size_bytes']:,} bytes (11.13x), {n_shared} tokens")
for ar in agent_receipts:
print(
f"[multi-agent] agent {ar['agent_id']}: {ar['num_unique_tokens']} unique tokens, "
f"{ar['shell_size_bytes']:,} bytes shell (digest {ar['shell_digest'][:12]})"
)
print(f"[multi-agent] loading model: {args.model}")
model = AutoModelForCausalLM.from_pretrained(
args.model, torch_dtype=torch.float16, low_cpu_mem_usage=False
).to(args.device)
model.eval()
cfg = model.config
num_attention_heads = getattr(cfg, "num_attention_heads", None)
head_dim = (
cfg.head_dim
if hasattr(cfg, "head_dim")
else (cfg.hidden_size // num_attention_heads if num_attention_heads else 64)
)
print(
f"[multi-agent] model: num_layers={cfg.num_hidden_layers} "
f"num_kv_heads={cfg.num_key_value_heads} head_dim={head_dim} hidden={cfg.hidden_size}"
)
print("[multi-agent] loading shared_kv.bin...")
shared_manifest, shared_layers_kv = load_kv_binary(args.multi_agent_dir / "shared_kv.bin")
print(
f"[multi-agent] shared KV: {shared_manifest['num_layers']} layers, "
f"{shared_manifest['num_tokens']} tokens, head_dim={shared_manifest['head_dim']}"
)
agent_caches = []
for i, ar in enumerate(agent_receipts):
print(f"[multi-agent] building cache for {ar['agent_id']}...")
agent_manifest, agent_layers_kv = load_kv_binary(
args.multi_agent_dir / f"agent_{i}_kv.bin"
)
full_layers = []
for sl_kv, al_kv in zip(shared_layers_kv, agent_layers_kv):
full_k = sl_kv[0] + al_kv[0]
full_v = sl_kv[1] + al_kv[1]
full_layers.append((full_k, full_v))
cache = build_cache_from_kv(
full_layers,
num_tokens=n_shared + ar["num_unique_tokens"],
num_kv_heads=cfg.num_key_value_heads,
head_dim=head_dim,
device=args.device,
dtype=torch.float16,
)
agent_caches.append((ar["agent_id"], cache, ar))
print(f"\n[multi-agent] PHASE A: oracle per-agent PPL")
oracle_per_agent = {}
for agent_id, _, ar in agent_caches:
pass
print(f"\n[multi-agent] PHASE B: per-agent PPL (shared pool + agent shell)")
results_per_agent = []
for i, (agent_id, cache, ar) in enumerate(agent_caches):
t0 = time.time()
with torch.no_grad():
out = model(
input_ids=input_ids.to(args.device),
past_key_values=cache,
use_cache=False,
)
forward_ms = (time.time() - t0) * 1000
logits = out.logits tail_len = ar["num_unique_tokens"]
eval_start = args.n_tokens - tail_len
shift_logits = logits[..., eval_start - 1 : -1, :].contiguous().float()
shift_labels = input_ids[..., eval_start:].to(args.device).contiguous()
nll_total = 0.0
n_tokens = 0
chunk = 64
for ci in range(0, shift_labels.size(1), chunk):
sl = shift_logits[:, ci : ci + chunk, :]
st = shift_labels[:, ci : ci + chunk]
log_probs = torch.nn.functional.log_softmax(sl, dim=-1)
nll = -log_probs.gather(2, st.unsqueeze(-1)).squeeze(-1)
nll_total += nll.sum().item()
n_tokens += st.numel()
roundtrip_ppl = torch.tensor(nll_total / n_tokens).exp().item()
print(
f"[multi-agent] {agent_id} roundtrip PPL: {roundtrip_ppl:.4f} "
f"(tail_len={tail_len}, forward={forward_ms:.0f}ms)"
)
results_per_agent.append({
"agent_id": agent_id,
"tail_len": tail_len,
"roundtrip_ppl": roundtrip_ppl,
"forward_ms": forward_ms,
})
print(f"\n[multi-agent] PHASE C: per-agent oracle PPL (standalone, no sharing)")
for r in results_per_agent:
tail_len = r["tail_len"]
eval_start = args.n_tokens - tail_len
t0 = time.time()
with torch.no_grad():
out = model(
input_ids=input_ids.to(args.device),
use_cache=False,
)
forward_ms = (time.time() - t0) * 1000
logits = out.logits.float()
shift_logits = logits[..., eval_start - 1 : -1, :].contiguous()
shift_labels = input_ids[..., eval_start:].to(args.device).contiguous()
nll_total = 0.0
n_tokens = 0
chunk = 64
for ci in range(0, shift_labels.size(1), chunk):
sl = shift_logits[:, ci : ci + chunk, :]
st = shift_labels[:, ci : ci + chunk]
log_probs = torch.nn.functional.log_softmax(sl, dim=-1)
nll = -log_probs.gather(2, st.unsqueeze(-1)).squeeze(-1)
nll_total += nll.sum().item()
n_tokens += st.numel()
oracle_ppl = torch.tensor(nll_total / n_tokens).exp().item()
delta_pct = (r["roundtrip_ppl"] - oracle_ppl) / oracle_ppl * 100
r["oracle_ppl"] = oracle_ppl
r["delta_ppl_pct"] = delta_pct
print(
f"[multi-agent] {r['agent_id']} oracle PPL: {oracle_ppl:.4f} | "
f"delta: {delta_pct:+.4f}%"
)
shared_bytes = shared_pool_receipt["pool_size_bytes"]
shells_bytes = [ar["shell_size_bytes"] for ar in agent_receipts]
total_with_sharing = shared_bytes + sum(shells_bytes)
raw_bytes_per_agent = (
cfg.num_hidden_layers
* cfg.num_key_value_heads
* args.n_tokens
* head_dim
* 2
* 2
)
naive_total = raw_bytes_per_agent * n_agents
memory_reduction = naive_total / total_with_sharing
state = {
"schema_version": "1.0.0",
"model": args.model,
"model_slug": args.model_slug,
"corpus": args.corpus,
"n_tokens": args.n_tokens,
"n_agents": n_agents,
"shared_frac": args.shared_frac,
"shared_pool": shared_pool_receipt,
"agents": results_per_agent,
"memory_accounting": {
"shared_pool_bytes": shared_bytes,
"shells_bytes": shells_bytes,
"total_with_sharing_bytes": total_with_sharing,
"raw_bytes_per_agent": raw_bytes_per_agent,
"naive_total_bytes": naive_total,
"memory_reduction_factor": memory_reduction,
},
"model_config": {
"num_layers": cfg.num_hidden_layers,
"num_kv_heads": cfg.num_key_value_heads,
"head_dim": head_dim,
},
"completed_at": datetime.datetime.now().isoformat(),
}
args.output.parent.mkdir(parents=True, exist_ok=True)
with open(args.output, "w") as f:
json.dump(state, f, indent=2)
print(f"\n[multi-agent] wrote {args.output}")
print(
f"[multi-agent] memory: shared {shared_bytes:,} + {n_agents} shells "
f"({sum(shells_bytes):,}) = {total_with_sharing:,} bytes; naive {naive_total:,} bytes "
f"({memory_reduction:.2f}x reduction)"
)
if __name__ == "__main__":
main()