| name | xquant-kv-cache-rematerialization |
| title | XQuant: Breaking Memory Wall for LLM Inference with KV Cache Rematerialization |
| version | 0.0.2 |
| engine | skillxiv-v0.0.2-claude-opus-4.6 |
| license | MIT |
| url | https://arxiv.org/abs/2508.10395 |
| keywords | ["inference-optimization","kv-cache","quantization","memory-efficient","rematerialization"] |
| description | Reduce KV cache memory by 7.7-10x through quantization and rematerialization of input activations instead of caching Keys and Values, trading recomputation for memory efficiency. |
XQuant: Breaking Memory Wall for LLM Inference with KV Cache Rematerialization
Core Concept
LLM inference is increasingly memory-bound: compute throughput far exceeds memory bandwidth, making KV cache memory the bottleneck. Traditional KV caching stores all Key and Value matrices from all layers and tokens, consuming enormous memory for long sequences.
XQuant breaks this bottleneck by storing only quantized layer input activations (X) instead of KV caches, then recomputing K and V on-the-fly during each forward pass. This trades modest computation increase for dramatic memory savings (7.7-10x), making long-sequence inference practical.
The key insight: input activations X are similar across layers and compress well; recomputing K = X·W_k is cheaper than storing/loading all cached KVs.
Architecture Overview
- Activation Caching: Store quantized input activations X instead of KV pairs
- On-the-Fly Recomputation: Recalculate K = X·W_k and V = X·W_v during inference
- Cross-Layer Sharing (XQuant-CL): Detect and reuse activations across similar layers
- Quantization: Compress cached X to 4-8 bits (per-channel or per-token)
- Negligible Accuracy Loss: Achieves 7.7x memory reduction with < 0.01 perplexity impact
Implementation Steps
1. Instrument Model to Capture Layer Inputs
Modify the model to save input activations at each layer without caching K, V matrices.
import torch
import torch.nn as nn
class CacheEnabledTransformer(nn.Module):
"""
Transformer with X-caching instead of KV caching
"""
def __init__(self, model):
super().__init__()
self.model = model
self.x_cache = {}
self.cache_enabled = False
def set_cache_mode(self, enabled):
"""Enable/disable caching during inference"""
self.cache_enabled = enabled
if enabled:
self.x_cache.clear()
def clear_cache(self):
"""Clear cached activations"""
self.x_cache.clear()
def forward(self, input_ids, attention_mask=None, use_cache=False):
"""
Forward pass with optional activation caching
"""
if use_cache:
self.set_cache_mode(True)
outputs = self.model(input_ids, attention_mask=attention_mask)
return outputs
2. Implement Quantization for Activations
Quantize layer input activations to reduce memory requirements.
class ActivationQuantizer:
"""
Quantize layer input activations for efficient caching
"""
def __init__(self, bits=8, method='per_channel'):
self.bits = bits
self.method = method
self.max_val = 2 ** bits - 1
def quantize(self, activation):
"""
Quantize activation tensor to fixed-point integers
activation: [batch, seq_len, hidden_size]
"""
if self.method == 'per_channel':
return self._quantize_per_channel(activation)
else:
return self._quantize_per_token(activation)
def _quantize_per_channel(self, activation):
"""
Quantize each hidden dimension independently
"""
batch, seq_len, hidden = activation.shape
min_vals = activation.reshape(-1, hidden).min(dim=0).values
max_vals = activation.reshape(-1, hidden).max(dim=0).values
ranges = max_vals - min_vals
ranges = ranges.clamp(min=1e-8)
scales = ranges / self.max_val
quantized = ((activation - min_vals.unsqueeze().unsqueeze()) / scales.unsqueeze().unsqueeze()).()
quantized = quantized.clamp(, .max_val).to(torch.uint8)
quantized, min_vals, scales
():
batch, seq_len, hidden = activation.shape
min_vals = activation.reshape(batch, seq_len, -).(dim=).values
max_vals = activation.reshape(batch, seq_len, -).(dim=).values
ranges = (max_vals - min_vals).clamp(=)
scales = ranges / .max_val
quantized = ((activation - min_vals.unsqueeze(-)) / scales.unsqueeze(-)).()
quantized = quantized.clamp(, .max_val).to(torch.uint8)
quantized, min_vals, scales
():
quantized.() * scales.unsqueeze(-) + min_vals.unsqueeze(-)
3. Cache Layer Inputs During Prefill
During the prefill (prompt processing) phase, cache quantized inputs instead of KV pairs.
class XCacheManager:
"""
Manages quantization and caching of layer inputs
"""
def __init__(self, quantizer, num_layers=32):
self.quantizer = quantizer
self.num_layers = num_layers
self.x_cache = {}
self.metadata = {}
def cache_layer_input(self, layer_idx, x):
"""
Cache quantized input activation for a layer
x: [batch, seq_len, hidden_size]
"""
quantized, min_vals, scales = self.quantizer.quantize(x)
self.x_cache[layer_idx] = quantized
self.metadata[layer_idx] = {
'min_vals': min_vals,
'scales': scales,
'dtype': x.dtype,
'shape': x.shape
}
def get_cached_x(self, layer_idx):
"""
Retrieve and dequantize cached activation
"""
if layer_idx not in self.x_cache:
return None
quantized = self.x_cache[layer_idx]
meta = self.metadata[layer_idx]
x = self.quantizer.dequantize(quantized, meta['min_vals'], meta['scales'])
return x
():
total_bytes =
layer_idx, quantized .x_cache.items():
bytes_per_element = .quantizer.bits /
cache_size = quantized.numel() * bytes_per_element
meta_size = .metadata[layer_idx][].numel() *
meta_size += .metadata[layer_idx][].numel() *
total_bytes += cache_size + meta_size
total_bytes / ( ** )
4. Implement On-the-Fly K, V Recomputation
During decoding, recompute K and V from cached X instead of loading them from cache.
class RematerializedAttention(nn.Module):
"""
Attention with on-the-fly KV recomputation from cached X
"""
def __init__(self, hidden_size, num_heads):
super().__init__()
self.hidden_size = hidden_size
self.num_heads = num_heads
self.head_dim = hidden_size // num_heads
self.w_k = nn.Linear(hidden_size, hidden_size)
self.w_v = nn.Linear(hidden_size, hidden_size)
self.w_q = nn.Linear(hidden_size, hidden_size)
self.w_o = nn.Linear(hidden_size, hidden_size)
def forward(self, x_cached, query_input, attention_mask=None):
"""
Compute attention with recomputed K, V from cached X
Args:
x_cached: cached quantized input activations [batch, seq_len, hidden]
query_input: current query input [batch, 1, hidden]
attention_mask: optional attention mask
"""
K = self.w_k(x_cached)
V = self.w_v(x_cached)
Q = self.w_q(query_input)
batch, seq_len, hidden = K.shape
K = K.view(batch, seq_len, self.num_heads, self.head_dim).transpose(1, 2)
V = V.view(batch, seq_len, self.num_heads, .head_dim).transpose(, )
Q = Q.view(batch, , .num_heads, .head_dim).transpose(, )
scores = torch.matmul(Q, K.transpose(-, -)) / (.head_dim ** )
attention_mask :
scores = scores.masked_fill(attention_mask == , ())
attn_weights = torch.softmax(scores, dim=-)
context = torch.matmul(attn_weights, V)
context = context.transpose(, ).contiguous()
context = context.view(batch, , hidden)
output = .w_o(context)
output
5. Implement Cross-Layer Sharing (XQuant-CL)
Detect similar activations across layers and reuse cache entries to save memory further.
class CrossLayerActivationAnalyzer:
"""
Analyze and identify shareable activations across layers
"""
def __init__(self, similarity_threshold=0.95):
self.similarity_threshold = similarity_threshold
self.layer_similarities = {}
def compute_layer_similarity(self, x_cache_dict):
"""
Compute cosine similarity between layer input activations
"""
layer_indices = sorted(x_cache_dict.keys())
for i, layer_i in enumerate(layer_indices):
for layer_j in layer_indices[i + 1:]:
x_i = x_cache_dict[layer_i]
x_j = x_cache_dict[layer_j]
if x_i.shape[1] > 100:
indices = torch.randperm(x_i.shape[1])[:100]
x_i_sample = x_i[:, indices, :]
x_j_sample = x_j[:, indices, :]
else:
x_i_sample = x_i
x_j_sample = x_j
x_i_flat = x_i_sample.reshape(-1, x_i_sample.shape[-1])
x_j_flat = x_j_sample.reshape(-1, x_j_sample.shape[-1])
cos_sim = torch.nn.functional.cosine_similarity(x_i_flat, x_j_flat).mean()
self.layer_similarities[(layer_i, layer_j)] = cos_sim.item()
():
shared_plan = {}
(layer_i, layer_j), similarity .layer_similarities.items():
similarity > .similarity_threshold:
layer_i shared_plan:
shared_plan[layer_i] = [layer_i]
shared_plan[layer_i].append(layer_j)
shared_plan
6. Inference Loop with X-Caching
Implement the full inference pipeline using activation caching and rematerialization.
def inference_with_xquant(model, input_ids, max_length=256, x_cache_manager=None):
"""
Run inference with X-caching and on-the-fly KV recomputation
"""
batch_size = input_ids.shape[0]
device = input_ids.device
with torch.no_grad():
current_input = input_ids
for layer_idx in range(model.num_layers):
layer = model.layers[layer_idx]
x = layer.pre_norm(current_input)
x_cache_manager.cache_layer_input(layer_idx, x)
current_input = layer(current_input)
generated_tokens = input_ids.clone()
for step in range(max_length - input_ids.shape[1]):
with torch.no_grad():
current_input = generated_tokens[:, -1:, :]
for layer_idx in range(model.num_layers):
layer = model.layers[layer_idx]
x_cached = x_cache_manager.get_cached_x(layer_idx)
x = layer.pre_norm(current_input)
attn_output = layer.rematerialized_attn(x_cached, x)
current_input = layer.post_norm(attn_output + current_input)
logits = model.head(current_input)
next_token = logits.argmax(dim=-1)
generated_tokens = torch.cat([generated_tokens, next_token], dim=1)
next_token.item() == model.eos_token_id:
generated_tokens
Practical Guidance
Hyperparameters & Configuration
- Quantization Bits: 8-bit per-channel recommended (good speed-quality tradeoff)
- Quantization Method: per-channel better than per-token (less overhead)
- Cross-Layer Threshold: 0.95 cosine similarity to identify shareable activations
- Memory Overhead: ~5-10% for metadata (scales, min_vals) per layer
- Speed Overhead: ~10-15% slower per-token inference due to K,V recomputation
When to Use XQuant
- Inference memory is the bottleneck (long sequences, large batch sizes)
- You can tolerate modest speed reduction for dramatic memory savings
- You need to fit very long sequences in limited GPU memory
- KV cache dominates memory usage (> 50% of peak memory)
- Per-token latency is not the primary concern
When NOT to Use XQuant
- You're optimizing for absolute latency (XQuant adds recomputation overhead)
- Your sequences are already short (< 4K tokens)
- You have abundant memory (KV caching is sufficient)
- Perplexity degradation cannot be tolerated
- You need cached KVs for speculative decoding or similar
Common Pitfalls
- Over-Aggressive Quantization: 4-bit quantization sometimes causes perplexity issues. Start with 8-bit.
- Not Profiling Memory: Don't assume X-caching helps without measuring actual peak memory.
- Ignoring Recomputation Cost: Recomputing K,V isn't free. Profile latency before deploying.
- Missing Cross-Layer Sharing: XQuant-CL can cut memory further if layer similarities are high. Analyze before training.
- No Baseline Comparison: Always compare end-to-end performance (accuracy + speed + memory) vs standard KV caching.
Reference
XQuant (2508.10395): https://arxiv.org/abs/2508.10395
Trade computation for memory by caching quantized layer inputs and recomputing K,V on-the-fly, achieving 7.7-10x memory reduction with minimal accuracy loss and enabling long-sequence inference on constrained hardware.