--- license: mit tags: - kernel - gguf - quantization --- # GGUF kernels Compute directly on the packed blocks of a GGUF checkpoint. A GGUF weight is stored as blocks of 32 or 256 values that share a scale. These kernels read that layout as it is, so a quantized model runs without ever materializing a dense copy of its weights — which is where the memory saving comes from. A 4B model at Q4_K_M stays at 3.1 GB instead of the 8.4 GB it would take unpacked. Ported from [llama.cpp](https://github.com/ggml-org/llama.cpp)'s `ggml-cuda`. `vendor/UPSTREAM` pins the revision the sources come from; `vendor.py` refreshes them. ## Usage ```python import torch from kernels import get_kernel gguf_kernels = get_kernel("marcsun13/gguf-kernels") # `blocks` is a GGUF weight exactly as stored: (out_features, bytes_per_row) uint8 out = gguf_kernels.mul_mat_vec(blocks, x, ggml_type=12, out_features=4096) # x @ blocks.T dense = gguf_kernels.dequantize(blocks, ggml_type=12, rows=4096, cols=2560, dtype=torch.bfloat16) ``` | | | | --- | --- | | `mul_mat_vec(blocks, x, ggml_type, out_features)` | Fused dequantize-gemv, for `x` of at most `MAX_GEMV_ROWS` rows (decode). Returns f32 whatever `x` was: the kernel writes an f32 destination, and casting at the call site lets the cast fuse into the consumer. | | `dequantize(blocks, ggml_type, rows, cols, dtype)` | Blocks to values, in `dtype` directly. For more rows than the gemv handles (prefill), unpack and use an ordinary matmul. | | `MAX_GEMV_ROWS` | Upstream's `MMVQ_MAX_BATCH_SIZE`. Beyond this the gemv has no implementation. | | `GEMV_TYPES` | The ggml type ids `mul_mat_vec` implements. `dequantize` covers more, so check this before choosing the fused path. | Both ops carry `register_fake` implementations, so they trace under `torch.compile` instead of breaking the graph — which matters, since a break at every quantized linear costs more than the kernels save. ## Backends CUDA today. The ops are named for what they do rather than for a backend: the schema is declared once in `torch-ext/torch_binding.cpp` and each backend registers its own implementation of it, so adding Metal means adding a `[kernel.*]` section and a source directory — no change to the schema or to the Python API, and callers dispatch on the tensor's device as usual. `mmq` (quantized gemm) and `mmf` (dense gemm) are deliberately not ported: both take a `ggml_backend_cuda_context` for their pool allocator, which would pull in the whole ggml backend. Above the gemv's row limit, unpacking and using cuBLAS measures at parity with llama.cpp anyway. ## Building ```bash nix run .#build-and-copy -L # every variant, into build/ nix build .#ci # just this system's variant pytest tests/ # against a built variant on PYTHONPATH ``` `flake.nix` tracks the builder's default branch, so CI builds the current variant matrix. The `build/` directory here was produced against the builder release matching the `kernels` client it was verified with (torch 2.11/2.12): both ops tested against `gguf.quants.dequantize` for Q4_K/Q5_K/Q6_K/ Q8_0 in f32/f16/bf16, the gemv against a dense matmul, and one `fullgraph=True` compile.