Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions bench/dense_vs_stc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import torch
import time
import numpy as np
from xlstm.blocks.mlstm.cell import mLSTMCell, mLSTMCellConfig

def benchmark_cell(mode="dense", B=1, NH=4, DH=32):
config = mLSTMCellConfig(
embedding_dim=NH*DH,
num_heads=NH,
context_length=256,
memory_backend="stc_sparse" if mode == "stc_sparse" else "dense",
gate_mode="ternary" if mode == "stc_sparse" else "sigmoid" # testing both if stc_sparse
)
cell = mLSTMCell(config)
cell.eval()

q = torch.randn(B, 1, NH*DH)
k = torch.randn(B, 1, NH*DH)
v = torch.randn(B, 1, NH*DH)

# Warmup
for _ in range(10):
_ = cell.step(q, k, v)

torch.cuda.synchronize() if torch.cuda.is_available() else None
start = time.time()
iters = 100
for _ in range(iters):
_ = cell.step(q, k, v)
torch.cuda.synchronize() if torch.cuda.is_available() else None
end = time.time()

latency = (end - start) / iters * 1000 # ms
tokens_per_sec = 1000 / latency

# Calculate sparsity if stc_sparse
sparsity = 0
if mode == "stc_sparse":
# We can't easily get it out without modifying the code or using hooks,
# but we can estimate it based on the threshold.
# Q(x) = 0 if |x| <= 0.1 * EMA(|x|)
# For Gaussian, P(|x| < 0.1 * E|x|) is small.
# But we can measure it by manually quantizing.
k_scaled = k.view(B, 1, NH, DH) / (DH**0.5)
k_q = cell.k_quantizer(k_scaled)
v_q = cell.v_quantizer(v.view(B, 1, NH, DH))
sparsity = ( (k_q == 0).float().mean() + (v_q == 0).float().mean() ) / 2

return latency, tokens_per_sec, sparsity

if __name__ == "__main__":
print(f"{'Mode':<15} | {'Latency (ms)':<15} | {'Tokens/sec':<15} | {'Sparsity':<15}")
print("-" * 65)

l_dense, t_dense, _ = benchmark_cell(mode="dense")
print(f"{'Dense (Baseline)':<15} | {l_dense:<15.4f} | {t_dense:<15.2f} | {'0.00':<15}")

l_stc, t_stc, s_stc = benchmark_cell(mode="stc_sparse")
print(f"{'STC Sparse':<15} | {l_stc:<15.4f} | {t_stc:<15.2f} | {s_stc:<15.4f}")
33 changes: 33 additions & 0 deletions bench/flops_saved.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import torch
import numpy as np

def estimate_flops(B=1, NH=4, DH=32, update_sparsity=0.9, gate_sparsity=0.5):
# Dense update: C_new = fg * C_prev + ig * (k @ v^T)
# Total: 4 * DH * DH per batch/head
dense_flops = B * NH * 4 * DH * DH

# STC Sparse update + Ternary Gate:
# If gate_sparsity is s_g, then with probability s_g (gate == 0), we do 0 ops.
# Otherwise, we do:
# 1. Scale C_prev: DH * DH
# 2. Update (outer product + scale + add): 3 * (1 - update_sparsity)^2 * DH * DH

# Prob(gate != 0) = (1 - s_g)
sparse_ops = B * NH * (1 - gate_sparsity) * (DH * DH + 3 * (1 - update_sparsity)**2 * DH * DH)

# Minimum ops for quantization (always performed)
quant_ops = B * NH * 3 * DH # 3 because of k, v, and gate
sparse_ops += quant_ops

savings = (dense_flops - sparse_ops) / dense_flops * 100
speedup_potential = dense_flops / sparse_ops

return dense_flops, sparse_ops, savings, speedup_potential

if __name__ == "__main__":
print(f"{'Update Spar':<12} | {'Gate Spar':<10} | {'Dense FLOPs':<12} | {'Sparse FLOPs':<12} | {'Savings %':<10} | {'Speedup':<10}")
print("-" * 80)
for s_u in [0.9, 0.95]:
for s_g in [0.5, 0.7, 0.9, 0.95, 0.99]:
d, sp, sav, speedup = estimate_flops(update_sparsity=s_u, gate_sparsity=s_g)
print(f"{s_u:<12.2f} | {s_g:<10.2f} | {d:<12d} | {int(sp):<12d} | {sav:<10.2f} | {speedup:<10.2f}x")
55 changes: 55 additions & 0 deletions design/stc_kernel_spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Sparse Ternary Covariance (STC) Kernel Specification

## Objective
Implement an optional backend for the mLSTM matrix memory update that uses sparse ternary quantization to achieve write-skip acceleration.

## Ternary Quantization Rule
The activations $v$ and $k$ are quantized into the ternary set $\{-1, 0, 1\}$ using a symmetric thresholding rule:

$$Q(x) =
\begin{cases}
1 & x > \tau \\
0 & |x| \le \tau \\
-1 & x < -\tau
\end{cases}$$

## Adaptive Thresholding
To maintain numerical stability across layers and training steps, the threshold $\tau$ must be adaptive based on the magnitude of the activations.

$$ \tau_t = 0.1 \cdot \text{EMA}(|x|) $$

where $\text{EMA}$ is an exponential moving average over recent activation magnitudes.

## Sparse Update Logic (STC Backend)
The dense update:
$$ C_{t} = \lambda C_{t-1} + \text{outer}(k, v) $$

is transformed into:
$$ v_q = Q(v) $$
$$ k_q = Q(k) $$
$$ C_{t} = \lambda C_{t-1} + v_q k_q^T $$

## Write-Skip Principle
The core performance optimization is to skip updates to $C_{ij}$ if either $v_i = 0$ or $k_j = 0$.

```python
for i in nonzero(v_q):
for j in nonzero(k_q):
C[i, j] += v_q[i] * k_q[j]
```

At 90% sparsity, this should result in a 10x reduction in write operations to the covariance matrix.

## Straight-Through Estimator (STE)
During training, gradients must be passed through the quantizer to ensure stability.

```python
class TernaryQuantSTE(torch.autograd.Function):
@staticmethod
def forward(ctx, x, tau):
return Q(x, tau)

@staticmethod
def backward(ctx, grad_output):
return grad_output.clone(), None
```
31 changes: 31 additions & 0 deletions docs/stc_recon.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Sparse Ternary Covariance (STC) Reconnaissance Report

## mLSTM Matrix Memory Update Implementation
The core matrix memory update logic for mLSTM is implemented in the recurrent step backend.

- **File Path:** `xlstm/xlstm/blocks/mlstm/backends.py`
- **Function:** `recurrent_step_stabilized_simple`
- **Tensor Shapes:**
- `q, k, v`: `(B, NH, DH, 1)` (after squeezing/unsqueezing in the function)
- `c_state`: `(B, NH, DH, DH)`
- `n_state`: `(B, NH, DH, 1)`
- `m_state`: `(B, NH, 1, 1)`
- **Update Path (Covariance):**
```python
c_state_new = fg_act * c_state + ig_act * (k_scaled @ v.transpose(-1, -2))
```
This is the dense outer-product update $C_t = \lambda C_{t-1} + v_t k_t^T$.

## Existing Benchmarks / Profiling
- **Experiments:** `experiments/main.py` (Parity task, Multi-Query Associative Recall).
- **Tests:** `tests/test_chunkwise_vs_recurrent.py` and `tests/template_chunkwise_vs_recurrent.py` compare different backends for numerical parity.
- **Profiling:** No dedicated profiling suite found, but `tests/template_chunkwise_vs_recurrent.py` can be adapted for latency measurements.

## Extension Infrastructure
- **Triton Kernels:** Present in `xlstm/xlstm_large` (requires GPU).
- **CUDA Kernels:** Mentioned for sLSTM in `xlstm/xlstm/blocks/slstm`.
- **Backend Selection:** `mLSTMCell` and `mLSTMLayer` allow selecting different backends via config/attributes.

## Benchmark Entry Points
- `experiments/main.py` for high-level task performance.
- `tests/template_chunkwise_vs_recurrent.py` for low-level kernel comparison.
27 changes: 26 additions & 1 deletion xlstm/blocks/mlstm/backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,11 @@ def recurrent_step_stabilized_simple(
igate_preact: torch.Tensor,
fgate_preact: torch.Tensor,
eps: float = 1e-6,
memory_backend: str = "dense",
k_quantizer: Optional[torch.nn.Module] = None,
v_quantizer: Optional[torch.nn.Module] = None,
gate_mode: str = "sigmoid",
ternary_gate: Optional[torch.nn.Module] = None,
**kwargs,
) -> tuple[torch.Tensor, tuple[torch.Tensor, torch.Tensor]]:
"""This is a single step of the mLSTM operation in recurrent form.
Expand All @@ -113,6 +118,11 @@ def recurrent_step_stabilized_simple(
v (torch.Tensor): (B, NH, 1, DH)
igate_preact (torch.Tensor): (B, NH, 1, 1)
fgate_preact (torch.Tensor): (B, NH, 1, 1)
memory_backend (str): "dense" or "stc_sparse"
k_quantizer (nn.Module): Optional quantizer for keys
v_quantizer (nn.Module): Optional quantizer for values
gate_mode (str): "sigmoid" or "ternary"
ternary_gate (nn.Module): Optional ternary gate module

Returns:
tuple[torch.Tensor, tuple[torch.Tensor, torch.Tensor]]:
Expand All @@ -133,7 +143,22 @@ def recurrent_step_stabilized_simple(

k_scaled = k / math.sqrt(DH)

c_state_new = fg_act * c_state + ig_act * (k_scaled @ v.transpose(-1, -2)) # (B, NH, DH, DH)
if memory_backend == "stc_sparse":
assert k_quantizer is not None and v_quantizer is not None, "Quantizers must be provided for stc_sparse backend."
k_q = k_quantizer(k_scaled)
v_q = v_quantizer(v)
from ...kernels.stc_sparse_update import stc_sparse_update
update = stc_sparse_update(None, k_q, v_q, None, None) # This logic needs adjustment
# Actually stc_sparse_update should return ONLY the update (outer product)
else:
update = k_scaled @ v.transpose(-1, -2)

if gate_mode == "ternary":
assert ternary_gate is not None, "ternary_gate module must be provided for ternary gate mode."
c_state_new = ternary_gate(c_state, update, fgate_preact) # Using fgate_preact as gate_input
else:
c_state_new = fg_act * c_state + ig_act * update # (B, NH, DH, DH)

n_state_new = fg_act * n_state + ig_act * k_scaled # (B, NH, DH, 1)

h_num = q.transpose(-1, -2) @ c_state_new # (B, NH, 1, DH)
Expand Down
16 changes: 16 additions & 0 deletions xlstm/blocks/mlstm/cell.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ class mLSTMCellConfig:
context_length: int = -1
embedding_dim: int = -1
num_heads: int = -1
memory_backend: str = "dense" # "dense" or "stc_sparse"
gate_mode: str = "sigmoid" # "sigmoid" or "ternary"


class mLSTMCell(nn.Module):
Expand All @@ -30,6 +32,15 @@ def __init__(self, config: mLSTMCellConfig):
self.igate = nn.Linear(3 * config.embedding_dim, config.num_heads)
self.fgate = nn.Linear(3 * config.embedding_dim, config.num_heads)

if config.memory_backend == "stc_sparse":
from ...modules.ternary_quantizer import TernaryQuantizer
self.k_quantizer = TernaryQuantizer()
self.v_quantizer = TernaryQuantizer()

if config.gate_mode == "ternary":
from ...modules.ternary_gate import TernaryGate
self.ternary_gate = TernaryGate()

self.outnorm = MultiHeadLayerNorm(ndim=config.embedding_dim, weight=True, bias=False)

self.register_buffer(
Expand Down Expand Up @@ -123,6 +134,11 @@ def step(
v=v,
igate_preact=igate_preact,
fgate_preact=fgate_preact,
memory_backend=self.config.memory_backend,
k_quantizer=getattr(self, "k_quantizer", None),
v_quantizer=getattr(self, "v_quantizer", None),
gate_mode=self.config.gate_mode,
ternary_gate=getattr(self, "ternary_gate", None),
) # (B, NH, 1 DH), ((B, NH, DH, DH), (B, NH, DH, 1), (B, NH, 1, 1))

h_state_norm = self.outnorm(h_state) # (B, NH, S, DH)
Expand Down
4 changes: 4 additions & 0 deletions xlstm/blocks/mlstm/layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ class mLSTMLayerConfig(UpProjConfigMixin):
qkv_proj_blocksize: int = 4
num_heads: int = 4
proj_factor: float = 2.0
memory_backend: str = "dense"
gate_mode: str = "sigmoid"

# will be set toplevel config
embedding_dim: int = -1
Expand Down Expand Up @@ -84,6 +86,8 @@ def __init__(self, config: mLSTMLayerConfig):
context_length=self.config.context_length,
embedding_dim=self.config._inner_embedding_dim,
num_heads=self.config.num_heads,
memory_backend=self.config.memory_backend,
gate_mode=self.config.gate_mode,
)
)
self.ogate_act_fn = nn.SiLU()
Expand Down
29 changes: 29 additions & 0 deletions xlstm/kernels/stc_sparse_update.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#include <torch/extension.h>

torch::Tensor stc_sparse_update_cuda(
torch::Tensor c_state,
torch::Tensor k_q,
torch::Tensor v_q,
torch::Tensor fg_act,
torch::Tensor ig_act
);

torch::Tensor stc_sparse_update(
torch::Tensor c_state,
torch::Tensor k_q,
torch::Tensor v_q,
torch::Tensor fg_act,
torch::Tensor ig_act
) {
if (c_state.is_cuda()) {
return stc_sparse_update_cuda(c_state, k_q, v_q, fg_act, ig_act);
} else {
// Fallback to PyTorch's native operations for CPU
auto outer = torch::matmul(k_q, v_q.transpose(-1, -2));
return fg_act * c_state + ig_act * outer;
}
}

PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) {
m.def("forward", &stc_sparse_update, "STC Sparse Update forward");
}
29 changes: 29 additions & 0 deletions xlstm/kernels/stc_sparse_update.cu
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#include <torch/extension.h>
#include <cuda.h>
#include <cuda_runtime.h>

// Dummy CUDA kernel placeholder for STC sparse update with write-skip
__global__ void stc_sparse_update_kernel(
const float* c_state,
const float* k_q,
const float* v_q,
const float* fg_act,
const float* ig_act,
float* c_state_new,
int B, int NH, int DH
) {
// Each thread could handle one element of C (DH*DH per head)
// and skip the write if k_q[i] == 0 or v_q[j] == 0.
}

torch::Tensor stc_sparse_update_cuda(
torch::Tensor c_state,
torch::Tensor k_q,
torch::Tensor v_q,
torch::Tensor f_act,
torch::Tensor i_act
) {
auto c_new = torch::zeros_like(c_state);
// CUDA kernel launch would go here
return c_new;
}
24 changes: 24 additions & 0 deletions xlstm/kernels/stc_sparse_update.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Copyright (c) NXAI GmbH and its affiliates 2024
import torch

def stc_sparse_update(
c_state: torch.Tensor,
k_q: torch.Tensor,
v_q: torch.Tensor,
fg_act: torch.Tensor,
ig_act: torch.Tensor,
) -> torch.Tensor:
"""
Sparse Ternary Covariance (STC) update.

C_new = fg_act * C_prev + ig_act * (k_q @ v_q^T)

If c_state is None, returns (k_q @ v_q^T).
"""
# outer product (B, NH, DH, DH)
outer = k_q @ v_q.transpose(-1, -2)

if c_state is None:
return outer

return fg_act * c_state + ig_act * outer
Loading
Loading