dmitry-rov/tilesparse-repro / scripts /roofline_analysis.py
dmitry-rov's picture
download
raw
7.04 kB
"""
Roofline / arithmetic-intensity analysis for decode-time attention.
Reconstructs (independently of the TileSparse paper's code, which is not public)
the mechanism behind Claim 3: that Multi-head Latent Attention (MLA) KV-cache
compression, combined with Multi-Token Prediction (MTP) batching multiple query
tokens per decode step, pushes attention arithmetic intensity (AI = FLOPs/byte)
across the GPU roofline ridge point, moving decode-time attention from the
memory-bound regime (classic single-token autoregressive decoding) into the
compute-bound regime.
Derivation (standard causal decode attention, KV cache of length L, query block
of D tokens computed per forward step, n_h heads, head dim d_h, element size
`elt_size` bytes):
FLOPs = 4 * D * L * n_h * d_h (QK^T + softmax(QK^T)V, 2 flops/MAC each)
Bytes = 2 * L * n_h * d_h * elt_size (reading K and V from cache once, reused
across the D queries in the block)
AI = FLOPs / Bytes = 2*D / elt_size
Notably L, n_h, d_h all cancel: decode-attention AI depends only on the query
block size D (the MTP degree) and element size. This is why MTP alone raises AI
linearly in D regardless of context length.
For MLA, the KV cache stores a compressed latent (kv_lora_rank d_c + decoupled
RoPE dim d_r) shared across all heads instead of full per-head K/V, so:
Bytes_MLA = (d_c + d_r) * elt_size (no n_h factor -- latent is shared)
We keep the FLOPs estimate for the "absorbed" MLA decode matmuls at the same
order as standard MHA (the absorption trick folds the up-projection into the
per-head query/output projections, so FLOPs are comparable while bytes shrink
drastically) -- this is the documented mechanism in the DeepSeek-V2 report and
is the honest, reproducible part of the analysis; we do not have access to the
TileSparse paper's own FLOP accounting since the OpenReview PDF is behind a
Cloudflare Turnstile challenge we could not solve programmatically, and no
arXiv preprint or GitHub repo is public for this paper (verified via HF papers
search, OpenReview API, and the ICML 2026 virtual site).
"""
import json
import math
ELT_SIZE_BYTES = 2 # bf16
# DeepSeek-V2 published MLA dimensions (public architecture, used as a
# representative real MLA config since TileSparse's own model config is not
# available to us).
N_HEADS = 128
HEAD_DIM = 128
KV_LORA_RANK = 512 # d_c
QK_ROPE_HEAD_DIM = 64 # d_r (shared decoupled RoPE dim)
# GPU roofline specs: (peak bf16 TFLOPS, peak HBM bandwidth GB/s)
GPUS = {
"A10G (a10g-large)": (125.0, 600.0),
"L4 (l4x1)": (121.0, 300.0),
"L40S (l40sx1)": (362.0, 864.0),
"A100-80GB (a100-large)": (312.0, 2039.0),
"H100 (h200 class)": (989.0, 3350.0),
}
def ai_mha(d):
return 2 * d / ELT_SIZE_BYTES
def ai_mla(d):
mha_bytes_per_token = 2 * N_HEADS * HEAD_DIM * ELT_SIZE_BYTES
mla_bytes_per_token = (KV_LORA_RANK + QK_ROPE_HEAD_DIM) * ELT_SIZE_BYTES
compression_ratio = mha_bytes_per_token / mla_bytes_per_token
# FLOPs held at the same order as MHA (absorption trick); bytes shrink by
# compression_ratio -> AI scales up by that same ratio.
return ai_mha(d) * compression_ratio, compression_ratio
def ridge_point(gpu_tflops, gpu_bw_gbs):
return (gpu_tflops * 1e12) / (gpu_bw_gbs * 1e9) # FLOPs/byte
def main():
results = {"mha_bytes_per_token": 2 * N_HEADS * HEAD_DIM * ELT_SIZE_BYTES,
"mla_bytes_per_token": (KV_LORA_RANK + QK_ROPE_HEAD_DIM) * ELT_SIZE_BYTES}
_, compression_ratio = ai_mla(1)
results["mla_kv_compression_ratio_x"] = compression_ratio
print(f"MLA KV-cache compression vs full MHA: {compression_ratio:.1f}x fewer bytes/token")
print(f" MHA: {results['mha_bytes_per_token']} bytes/token (n_h={N_HEADS}, d_h={HEAD_DIM})")
print(f" MLA: {results['mla_bytes_per_token']} bytes/token (d_c={KV_LORA_RANK}, d_r={QK_ROPE_HEAD_DIM})")
print()
print(f"{'GPU':<24}{'ridge (FLOPs/B)':<18}{'D* for MHA':<14}{'D* for MLA':<14}")
gpu_rows = []
for name, (tflops, bw) in GPUS.items():
rp = ridge_point(tflops, bw)
d_star_mha = rp * ELT_SIZE_BYTES / 2 # solve ai_mha(D) = rp
d_star_mla = d_star_mha / compression_ratio # solve ai_mla(D) = rp
print(f"{name:<24}{rp:<18.1f}{d_star_mha:<14.2f}{d_star_mla:<14.2f}")
gpu_rows.append({
"gpu": name, "peak_tflops_bf16": tflops, "peak_bw_gbs": bw,
"ridge_flops_per_byte": rp,
"D_star_plain_MHA_decode": d_star_mha,
"D_star_MLA_decode": d_star_mla,
})
results["gpus"] = gpu_rows
print()
print("Interpretation: D* is the query-block size (MTP degree) at which decode")
print("attention crosses from memory-bound (AI < ridge) to compute-bound (AI > ridge).")
print("Plain single-token MHA decoding (D=1) is memory-bound on every GPU listed")
print(f"(needs D >= {min(r['D_star_plain_MHA_decode'] for r in gpu_rows):.1f} to become compute-bound, i.e. far beyond typical MTP degrees of 1-4).")
print(f"MLA cuts the required D* by {compression_ratio:.0f}x; on the fastest GPU (H100-class) MLA decode")
print(f"crosses into compute-bound territory at D* ~= {min(r['D_star_MLA_decode'] for r in gpu_rows):.2f}, i.e. MTP degree as low as 1-2 tokens.")
import os
os.makedirs("outputs", exist_ok=True)
with open("outputs/roofline_results.json", "w") as f:
json.dump(results, f, indent=2)
make_plot(gpu_rows, compression_ratio)
def make_plot(gpu_rows, compression_ratio):
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
d_vals = [2 ** i for i in range(0, 8)] # 1..128
fig, ax = plt.subplots(figsize=(6.5, 5.8))
ax.plot(d_vals, [ai_mha(d) for d in d_vals], "o-", label="Plain MHA decode AI(D)", linewidth=2, zorder=3)
ax.plot(d_vals, [ai_mha(d) * compression_ratio for d in d_vals], "s-", label="MLA decode AI(D)", linewidth=2, zorder=3)
# Only annotate the fastest/slowest GPU ridge (5 lines was illegible at
# poster scale); the rest are in outputs/roofline_results.json.
endpoints = {r["gpu"]: r for r in gpu_rows}
for name in ("A100-80GB (a100-large)", "H100 (h200 class)"):
row = endpoints[name]
ax.axhline(row["ridge_flops_per_byte"], linestyle="--", color="gray", alpha=0.6, zorder=1)
ax.annotate(f"{row['gpu'].split(' (')[0]} ridge", xy=(d_vals[0], row["ridge_flops_per_byte"]),
xytext=(2, 4), textcoords="offset points", fontsize=8, color="dimgray")
ax.set_xscale("log", base=2)
ax.set_yscale("log")
ax.set_xlabel("Query block size D (MTP degree)")
ax.set_ylabel("Arithmetic intensity (FLOPs/byte)")
ax.set_title("Decode-attention AI vs MTP degree D")
ax.legend(fontsize=9, loc="lower right", framealpha=0.9)
fig.tight_layout()
fig.savefig("outputs/roofline_plot.png", dpi=150)
print("\nSaved outputs/roofline_plot.png and outputs/roofline_results.json")
if __name__ == "__main__":
main()

Xet Storage Details

Size:
7.04 kB
·
Xet hash:
be1daee3a924088c631ed94af727bcc9bfa4d6be164684b965b91fcdcf571d42

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.