You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
ggml : add fused SINKHORN_NORM op (Sinkhorn-Knopp normalization) + use in DeepSeek-V4
## Summary
This adds a new ggml op, `GGML_OP_SINKHORN_NORM` (`ggml_sinkhorn_norm`), that performs a
fused **Sinkhorn-Knopp doubly-stochastic normalization** on small square matrices, and wires
it into the DeepSeek-V4 (mHC / manifold-constrained hyper-connections) graph.
DeepSeek-V4 makes its per-token, per-layer hyper-connection mixing matrix (`[hc, hc]`,
with `hc = 4`) doubly-stochastic by running Sinkhorn-Knopp (softmax, then alternating
row/column normalization) for `sinkhorn_iterations` (20 in the released model). Today this
is expressed as a decomposition of primitive ggml ops (`soft_max` + repeated
`sum_rows` / `add` / `div` / `cont(permute)` transpose), which expands to **~137 ops per
layer**. On the tiny 4x4 slices this is almost entirely kernel-launch and memory-round-trip
overhead with negligible arithmetic.
The new op computes the entire normalization in a single kernel (per layer), keeping the
matrix in registers across all iterations.
## Motivation / profiling
Profiling DeepSeek-V4-Flash (UD-Q4_K_XL, 284B, 43 layers) inference on an AMD MI250X
(gfx90a, ROCm/HIP) with `rocprofv3 --kernel-trace` showed the decomposed Sinkhorn dominating
GPU time:
- `k_bin_bcast` (broadcast div/add): ~31% of GPU kernel time, ~217k dispatches
- `reduce_rows_f32` (row sums): ~8%, ~67k dispatches
- `cpy_scalar_transpose`: ~4.5%, ~34k dispatches
Predicted vs. observed dispatch counts confirmed these are almost entirely the Sinkhorn
decomposition (e.g. transpose count matched to within <1%). In aggregate the normalization
was **~30% of total GPU kernel time** across ~250k tiny dispatches.
## Results (DeepSeek-V4-Flash, 4x MI250X GCDs, llama-bench, back-to-back A/B)
| variant | prefill (pp64) | decode (tg16) |
|----------------------------------|---------------:|--------------:|
| decomposed (baseline) | 111.5 t/s | 10.3 t/s |
| fused SINKHORN_NORM (this PR) | 119.8 t/s | 16.5 t/s |
**Decode +60.9%, prefill +7.4%** end-to-end on the full model.
## Implementation
- New op `GGML_OP_SINKHORN_NORM` + constructor
`ggml_sinkhorn_norm(ctx, a, n_iters, eps)` (op params: `n_iters` int32, `eps` f32).
Operates independently on each `[n, n]` slice (`ne0 == ne1 == n`): softmax over `ne0`,
add `eps`, one column normalization, then `n_iters - 1` alternating {row, column}
normalizations (each `+ eps`). The interface is general (arbitrary square `n`, configurable
`n_iters` / `eps`); DeepSeek-V4 is simply the first consumer.
- **CPU** reference implementation (`ggml-cpu/ops.cpp`), parallelized over slices.
- **CUDA/HIP** kernel (`ggml-cuda/sinkhorn-norm.cu`):
- warp-cooperative kernel for power-of-two `n` with `n*n <= warp_size`: one lane per matrix
element, fully coalesced global load/store, segmented `__shfl_xor` butterfly reductions
for the row/column sums;
- a generic one-thread-per-slice fallback otherwise.
- **Other backends** (Metal/Vulkan/SYCL/CANN/OpenCL): `supports_op` returns false for this
op (default), so the graph scheduler falls back to the CPU implementation. No crashes; a
dedicated kernel can be added later if desired.
- **Tests**: `test-backend-ops` cases for `SINKHORN_NORM` (n=4, tokens {1,7,128,1024},
iters {1,2,20}) plus perf cases; CPU-vs-CUDA all pass.
- DeepSeek-V4 (`src/models/deepseek4.cpp`) `build_hc_sinkhorn` now calls the fused op.
## Precedent
ggml already contains architecture-specific fused ops in the core op enum, e.g.
`GGML_OP_SSM_CONV` / `GGML_OP_SSM_SCAN` (Mamba), `GGML_OP_RWKV_WKV6` / `GGML_OP_RWKV_WKV7` /
`GGML_OP_GATED_LINEAR_ATTN` (RWKV). `SINKHORN_NORM` follows the same pattern for DeepSeek-V4,
but is defined as a general normalization primitive rather than a model-specific op.
## Test plan
- [ ] `test-backend-ops test -o SINKHORN_NORM` passes on CPU and CUDA/HIP (12/12 here).
- [ ] `test-backend-ops perf -o SINKHORN_NORM` runs.
- [ ] Full `test-backend-ops` shows no regressions (new op cleanly inserted into the enum).
- [ ] DeepSeek-V4 output unchanged vs. the decomposed graph (validated via the CPU reference).
- [ ] Confirm graceful CPU fallback on a non-CUDA backend (e.g. Metal/Vulkan).
## Notes for reviewers
- Open question / happy to adjust: keep this as a public `GGML_OP_SINKHORN_NORM`, or fold it
into an internal fused helper for DeepSeek-V4 only? Went with a general op given the
Mamba/RWKV precedent, but will follow maintainer preference.
- Warp kernel assumes `n` is a power of two and `n*n <= warp_size` (true for `hc = 4`);
the generic kernel covers everything else.
0 commit comments