Kalpana-API-GPU / benchmark_real.py
MaduRox
fix: support newer DynamicCache attribute structure in transformers 5.8
f4b26d5
Raw
History Blame Contribute Delete
19.2 kB
"""
Kalpana RIF — Real Empirical Benchmark Harness
================================================
Measures ACTUAL GPU memory, latency, and recall at multiple context lengths.
Compares: Standard DynamicCache vs KalpanaDynamicCache vs SinkCache (StreamingLLM).
All numbers are measured, not estimated.
CRITICAL NOTE on what is measured:
- persistent_cache_mb: The stored cache state size (O(1) for Kalpana)
- peak_vram_mb: PEAK GPU allocation including intermediate tensors during
forward pass — this includes reconstruction intermediates for Kalpana
- prefill_time_s: Wall clock to process all input tokens
- ttft_ms: Time to generate the FIRST output token after prefill
- avg_token_ms: Average time per generated token
- reconstruction_cosine_sim: Cosine similarity of Kalpana's reconstructed K/V
vs ground-truth standard cache K/V (measures information loss)
"""
import torch
import torch.nn.functional as F
import time
import json
import gc
import os
import traceback
MODEL_NAME = "Qwen/Qwen2.5-0.5B-Instruct"
# ---------------------------------------------------------------------------
# Haystack builder: long filler text with a planted "needle" fact
# ---------------------------------------------------------------------------
FILLER = (
"System telemetry block {i}: harmonic sensor reading at {f:.4f} MHz "
"with phase offset {p} degrees in monitoring sector {s}. "
"All parameters within nominal operating range. "
)
NEEDLE_TEMPLATE = (
"CRITICAL CLASSIFIED FINDING: The secret authorization passkey "
"for Project Nightingale is {code}. This information is top-secret. "
)
NEEDLE_QUERY = (
"What is the secret authorization passkey for Project Nightingale? "
"Reply with ONLY the passkey code, nothing else."
)
def build_haystack(tokenizer, target_tokens, needle_code, needle_depth_pct=0.5):
"""Build input_ids with a needle fact embedded at specified depth percentage."""
# Generate filler chunks
chunks = []
for i in range(30000):
chunks.append(FILLER.format(i=i, f=i * 0.31416, p=(i * 37) % 360, s=i % 16))
# Estimate tokens per filler chunk
sample_enc = tokenizer.encode(chunks[0], add_special_tokens=False)
toks_per_chunk = max(1, len(sample_enc))
# Calculate chunks needed (leave room for needle + query + template)
overhead_tokens = 120 # chat template + query + needle
content_tokens = max(10, target_tokens - overhead_tokens)
n_chunks = max(1, content_tokens // toks_per_chunk)
# Insert needle at target depth
needle_idx = max(0, int(n_chunks * needle_depth_pct))
needle_text = NEEDLE_TEMPLATE.format(code=needle_code)
chunks_to_use = chunks[:n_chunks]
chunks_to_use.insert(needle_idx, needle_text)
context = " ".join(chunks_to_use)
full_prompt = context + "\n\nQuestion: " + NEEDLE_QUERY
messages = [{"role": "user", "content": full_prompt}]
formatted = tokenizer.apply_chat_template(
messages, tokenize=False, add_generation_prompt=True
)
input_ids = tokenizer(
formatted, return_tensors="pt", truncation=True, max_length=target_tokens
).input_ids
return input_ids
# ---------------------------------------------------------------------------
# Core measurement function
# ---------------------------------------------------------------------------
def measure_one(model, tokenizer, input_ids, cache, cache_name, device, num_gen=10):
"""
Measure one benchmark point: prefill + generation.
Returns dict with all measured metrics.
"""
N = input_ids.shape[1]
# Clean slate
gc.collect()
torch.cuda.empty_cache()
torch.cuda.reset_peak_memory_stats(device)
baseline_vram = torch.cuda.memory_allocated(device)
# === PREFILL ===
t_prefill_start = time.perf_counter()
try:
with torch.inference_mode():
out = model(
input_ids.to(device), past_key_values=cache, use_cache=True
)
torch.cuda.synchronize()
except Exception as e:
gc.collect()
torch.cuda.empty_cache()
return {
"cache_type": cache_name,
"context_length": N,
"error": f"Prefill failed: {type(e).__name__}: {e}",
}
t_prefill_end = time.perf_counter()
peak_vram_prefill = torch.cuda.max_memory_allocated(device)
alloc_after_prefill = torch.cuda.memory_allocated(device)
# Persistent cache size
if hasattr(cache, "get_total_memory_mb"):
persist_mb = cache.get_total_memory_mb()
elif hasattr(cache, "key_cache"):
b = 0
for t in getattr(cache, "key_cache", []):
if isinstance(t, torch.Tensor):
b += t.nelement() * t.element_size()
for t in getattr(cache, "value_cache", []):
if isinstance(t, torch.Tensor):
b += t.nelement() * t.element_size()
persist_mb = b / (1024 * 1024)
elif hasattr(cache, "layers"):
b = 0
for layer in cache.layers:
if hasattr(layer, "keys") and isinstance(layer.keys, torch.Tensor):
b += layer.keys.nelement() * layer.keys.element_size()
if hasattr(layer, "values") and isinstance(layer.values, torch.Tensor):
b += layer.values.nelement() * layer.values.element_size()
persist_mb = b / (1024 * 1024)
else:
persist_mb = -1
# === GENERATION (token by token) ===
torch.cuda.reset_peak_memory_stats(device)
nxt = out.logits[:, -1:, :].argmax(dim=-1)
generated_ids = []
gen_times = []
for _ in range(num_gen):
t0g = time.perf_counter()
try:
with torch.inference_mode():
out = model(nxt, past_key_values=cache, use_cache=True)
torch.cuda.synchronize()
except Exception:
break
gen_times.append(time.perf_counter() - t0g)
nxt = out.logits[:, -1:, :].argmax(dim=-1)
generated_ids.append(nxt.item())
peak_vram_gen = torch.cuda.max_memory_allocated(device)
gen_text = tokenizer.decode(generated_ids, skip_special_tokens=True)
del out, nxt
return {
"cache_type": cache_name,
"context_length": N,
"persistent_cache_mb": round(persist_mb, 3),
"peak_vram_prefill_mb": round(peak_vram_prefill / (1024 ** 2), 2),
"peak_vram_generation_mb": round(peak_vram_gen / (1024 ** 2), 2),
"vram_delta_after_prefill_mb": round(
(alloc_after_prefill - baseline_vram) / (1024 ** 2), 2
),
"prefill_time_s": round(t_prefill_end - t_prefill_start, 4),
"prefill_tok_per_s": round(N / max(1e-6, t_prefill_end - t_prefill_start), 1),
"ttft_ms": round(gen_times[0] * 1000, 2) if gen_times else None,
"avg_token_ms": round(
sum(gen_times) / max(1, len(gen_times)) * 1000, 2
)
if gen_times
else None,
"tokens_generated": len(generated_ids),
"generated_text": gen_text[:300],
}
# ---------------------------------------------------------------------------
# Reconstruction fidelity: compare Kalpana K/V vs ground-truth
# ---------------------------------------------------------------------------
def measure_reconstruction_fidelity(model, tokenizer, input_ids, device, num_layers):
"""
Compare K/V tensors from standard DynamicCache vs KalpanaDynamicCache.
Returns per-layer cosine similarity.
"""
from transformers import DynamicCache
from kalpana_embed_to_kv import KalpanaDynamicCache
N = input_ids.shape[1]
# Run standard
gc.collect()
torch.cuda.empty_cache()
std_cache = DynamicCache()
with torch.inference_mode():
model(input_ids.to(device), past_key_values=std_cache, use_cache=True)
torch.cuda.synchronize()
# Capture standard K/V
if hasattr(std_cache, "key_cache"):
std_keys = [k.detach().clone() for k in getattr(std_cache, "key_cache", []) if isinstance(k, torch.Tensor)]
std_vals = [v.detach().clone() for v in getattr(std_cache, "value_cache", []) if isinstance(v, torch.Tensor)]
elif hasattr(std_cache, "layers"):
std_keys = [layer.keys.detach().clone() for layer in std_cache.layers if hasattr(layer, "keys") and isinstance(layer.keys, torch.Tensor)]
std_vals = [layer.values.detach().clone() for layer in std_cache.layers if hasattr(layer, "values") and isinstance(layer.values, torch.Tensor)]
else:
std_keys, std_vals = [], []
del std_cache
gc.collect()
torch.cuda.empty_cache()
# Run Kalpana
kal_cache = KalpanaDynamicCache(
num_layers=num_layers, bands=2048, sliding_window=128
)
with torch.inference_mode():
model(input_ids.to(device), past_key_values=kal_cache, use_cache=True)
torch.cuda.synchronize()
kal_keys = [k.detach().clone() for k in kal_cache.key_cache]
kal_vals = [v.detach().clone() for v in kal_cache.value_cache]
del kal_cache
gc.collect()
torch.cuda.empty_cache()
# Compare
layer_sims = []
for layer_idx in range(min(len(std_keys), len(kal_keys))):
sk = std_keys[layer_idx].float().flatten()
kk = kal_keys[layer_idx].float().flatten()
sv = std_vals[layer_idx].float().flatten()
kv = kal_vals[layer_idx].float().flatten()
# Shapes might differ if Kalpana hybrid has window + prefix
min_len_k = min(sk.shape[0], kk.shape[0])
min_len_v = min(sv.shape[0], kv.shape[0])
key_sim = F.cosine_similarity(sk[:min_len_k].unsqueeze(0), kk[:min_len_k].unsqueeze(0)).item()
val_sim = F.cosine_similarity(sv[:min_len_v].unsqueeze(0), kv[:min_len_v].unsqueeze(0)).item()
layer_sims.append({
"layer": layer_idx,
"key_cosine_sim": round(key_sim, 6),
"val_cosine_sim": round(val_sim, 6),
"std_key_shape": list(std_keys[layer_idx].shape),
"kal_key_shape": list(kal_keys[layer_idx].shape),
})
del std_keys, std_vals, kal_keys, kal_vals
gc.collect()
torch.cuda.empty_cache()
avg_key_sim = sum(l["key_cosine_sim"] for l in layer_sims) / max(1, len(layer_sims))
avg_val_sim = sum(l["val_cosine_sim"] for l in layer_sims) / max(1, len(layer_sims))
return {
"context_length": N,
"avg_key_cosine_sim": round(avg_key_sim, 6),
"avg_val_cosine_sim": round(avg_val_sim, 6),
"per_layer": layer_sims,
}
# ---------------------------------------------------------------------------
# Main benchmark runner
# ---------------------------------------------------------------------------
def run_benchmark(
context_lengths=None,
num_gen_tokens=10,
run_fidelity=True,
fidelity_lengths=None,
):
"""
Run the full benchmark suite.
Args:
context_lengths: list of int, token counts to test (default: [128..4096])
num_gen_tokens: how many tokens to generate per test
run_fidelity: whether to run reconstruction fidelity comparison
fidelity_lengths: context lengths for fidelity test (default: [128, 256, 512])
Returns:
dict with metadata and results
"""
if context_lengths is None:
context_lengths = [128, 256, 512, 1024, 2048, 4096]
if fidelity_lengths is None:
fidelity_lengths = [128, 256, 512]
device = "cuda" if torch.cuda.is_available() else "cpu"
dtype = torch.float16 if device == "cuda" else torch.float32
gpu_name = torch.cuda.get_device_name(0) if device == "cuda" else "CPU"
total_vram = (
torch.cuda.get_device_properties(0).total_memory / (1024 ** 3)
if device == "cuda"
else 0
)
from transformers import AutoModelForCausalLM, AutoTokenizer
print(f"[Benchmark] Loading {MODEL_NAME}...")
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
if tokenizer.pad_token_id is None:
tokenizer.pad_token_id = tokenizer.eos_token_id
model = AutoModelForCausalLM.from_pretrained(
MODEL_NAME, torch_dtype=dtype, low_cpu_mem_usage=True
).to(device)
model.eval()
model_vram = (
torch.cuda.memory_allocated(device) / (1024 ** 2) if device == "cuda" else 0
)
num_layers = getattr(model.config, "num_hidden_layers", 24)
num_kv_heads = getattr(model.config, "num_key_value_heads", 2)
head_dim = getattr(model.config, "head_dim", 64)
elem_bytes = 2 if dtype == torch.float16 else 4
# Theoretical KV bytes per token for standard cache
kv_bytes_per_token = num_layers * num_kv_heads * head_dim * 2 * elem_bytes
# Theoretical Kalpana persistent state size
# layers * (K+V) * heads * bands * dim * (real+imag) * fp32
kalpana_state_bytes = num_layers * 2 * num_kv_heads * 2048 * head_dim * 2 * 4
kalpana_state_mb = kalpana_state_bytes / (1024 ** 2)
meta = {
"gpu": gpu_name,
"total_vram_gb": round(total_vram, 1),
"model": MODEL_NAME,
"model_vram_mb": round(model_vram, 1),
"num_layers": num_layers,
"num_kv_heads": num_kv_heads,
"head_dim": head_dim,
"dtype": str(dtype),
"kv_bytes_per_token_standard": kv_bytes_per_token,
"kalpana_theoretical_state_mb": round(kalpana_state_mb, 2),
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S UTC", time.gmtime()),
}
print(f"[Benchmark] GPU: {gpu_name}, VRAM: {total_vram:.1f} GB")
print(f"[Benchmark] Model VRAM: {model_vram:.1f} MB")
print(f"[Benchmark] KV bytes/token (standard): {kv_bytes_per_token}")
print(f"[Benchmark] Kalpana theoretical state: {kalpana_state_mb:.2f} MB")
needle_code = "NIGHTINGALE-7749"
results = []
# ── Main scaling benchmark ──
for ctx_len in context_lengths:
print(f"\n{'=' * 60}")
print(f"CONTEXT LENGTH: {ctx_len} tokens")
print(f"{'=' * 60}")
input_ids = build_haystack(tokenizer, ctx_len, needle_code, needle_depth_pct=0.5)
actual = input_ids.shape[1]
print(f" Actual input tokens: {actual}")
# --- Standard DynamicCache ---
print(" [1/3] Standard DynamicCache...")
from transformers import DynamicCache
cache = DynamicCache()
r = measure_one(model, tokenizer, input_ids, cache, "Standard_DynamicCache", device, num_gen_tokens)
r["needle_code"] = needle_code
r["needle_found"] = needle_code.lower() in r.get("generated_text", "").lower()
r["theoretical_cache_mb"] = round(actual * kv_bytes_per_token / (1024 ** 2), 3)
results.append(r)
del cache
gc.collect()
torch.cuda.empty_cache()
print(f" prefill={r.get('prefill_time_s')}s peak={r.get('peak_vram_prefill_mb')}MB cache={r.get('persistent_cache_mb')}MB needle={r.get('needle_found')}")
# --- KalpanaDynamicCache ---
print(" [2/3] KalpanaDynamicCache (bands=2048, window=128)...")
try:
from kalpana_embed_to_kv import KalpanaDynamicCache
cache = KalpanaDynamicCache(
num_layers=num_layers, bands=2048, sliding_window=128
)
r = measure_one(model, tokenizer, input_ids, cache, "Kalpana_RIF", device, num_gen_tokens)
r["needle_code"] = needle_code
r["needle_found"] = needle_code.lower() in r.get("generated_text", "").lower()
r["kalpana_theoretical_state_mb"] = round(kalpana_state_mb, 3)
results.append(r)
del cache
gc.collect()
torch.cuda.empty_cache()
print(f" prefill={r.get('prefill_time_s')}s peak={r.get('peak_vram_prefill_mb')}MB persist={r.get('persistent_cache_mb')}MB needle={r.get('needle_found')}")
except Exception as e:
err_r = {
"cache_type": "Kalpana_RIF",
"context_length": actual,
"error": f"{type(e).__name__}: {e}",
}
results.append(err_r)
print(f" ERROR: {e}")
gc.collect()
torch.cuda.empty_cache()
# --- SinkCache (StreamingLLM) ---
print(" [3/3] SinkCache (StreamingLLM, window=128, sinks=4)...")
try:
from transformers import SinkCache
cache = SinkCache(window_length=128, num_sink_tokens=4)
r = measure_one(model, tokenizer, input_ids, cache, "SinkCache_StreamingLLM", device, num_gen_tokens)
r["needle_code"] = needle_code
r["needle_found"] = needle_code.lower() in r.get("generated_text", "").lower()
results.append(r)
del cache
gc.collect()
torch.cuda.empty_cache()
print(f" prefill={r.get('prefill_time_s')}s peak={r.get('peak_vram_prefill_mb')}MB cache={r.get('persistent_cache_mb')}MB needle={r.get('needle_found')}")
except ImportError:
results.append({
"cache_type": "SinkCache_StreamingLLM",
"context_length": actual,
"error": "SinkCache not available in this transformers version",
})
print(" SKIPPED (SinkCache not available)")
except Exception as e:
results.append({
"cache_type": "SinkCache_StreamingLLM",
"context_length": actual,
"error": f"{type(e).__name__}: {e}",
})
print(f" ERROR: {e}")
gc.collect()
torch.cuda.empty_cache()
# ── Reconstruction fidelity test ──
fidelity_results = []
if run_fidelity:
print(f"\n{'=' * 60}")
print("RECONSTRUCTION FIDELITY TEST")
print(f"{'=' * 60}")
for fl in fidelity_lengths:
if fl > max(context_lengths):
continue
print(f" Fidelity test at {fl} tokens...")
try:
input_ids = build_haystack(tokenizer, fl, needle_code)
fr = measure_reconstruction_fidelity(
model, tokenizer, input_ids, device, num_layers
)
fidelity_results.append(fr)
print(f" avg_key_sim={fr['avg_key_cosine_sim']:.6f} avg_val_sim={fr['avg_val_cosine_sim']:.6f}")
except Exception as e:
fidelity_results.append({
"context_length": fl,
"error": f"{type(e).__name__}: {e}",
})
print(f" ERROR: {e}")
gc.collect()
torch.cuda.empty_cache()
return {
"metadata": meta,
"scaling_results": results,
"fidelity_results": fidelity_results,
}
# ---------------------------------------------------------------------------
# Standalone entry point
# ---------------------------------------------------------------------------
if __name__ == "__main__":
import sys
result = run_benchmark()
out_path = os.path.join(os.path.dirname(__file__), "benchmark_results.json")
with open(out_path, "w") as f:
json.dump(result, f, indent=2, default=str)
print(f"\n\nResults saved to {out_path}")
print(json.dumps(result, indent=2, default=str))