|
Download docs/modules/f16.md from PYTHAI/bankml: direct link, hf CLI and curl.
- Browser
- Download file 8.6 kB
-
https://huggingface.co/spaces/PYTHAI/bankml/resolve/main/docs/modules/f16.md
- Command line
-
hf download hf://spaces/PYTHAI/bankml/docs/modules/f16.md
-
curl -L -o f16.md https://huggingface.co/spaces/PYTHAI/bankml/resolve/main/docs/modules/f16.md
8.6 kB
| # `bankML/f16.rs` — F16 weights, multiplied as the shipped ggml b11192 multiplies them | |
| ## Summary | |
| `f16.rs` (O3, added in 0.3.4) multiplies F16 weight matrices with the bits of the shipped `libggml-cpu-haswell.so` | |
| at b11192. It was read from that tag's `ggml-cpu.c` (`ggml_compute_forward_mul_mat`), `vec.cpp` (`ggml_vec_dot_f16`) | |
| and `llamafile/sgemm.cpp` (`llamafile_sgemm`, `tinyBLAS`). | |
| ggml multiplies an F16 weight two different ways, and the two give different bits for the same row and column: | |
| - **one column** (a decode step, the logits, a one-token micro-batch): `llamafile_sgemm` refuses `n < 2`, so every | |
| element is `ggml_vec_dot_f16`: four 8-lane accumulators over 32-element steps, a fixed reduction, then any tail | |
| (`n % 32`) added in double; | |
| - **two columns or more**: `tinyBLAS<8, __m256, …>` when the weight has a multiple of 4 rows and the row a multiple | |
| of 8; each element is one 8-lane FMA chain over the row, then `hsum`. Otherwise ggml falls back to `vec_dot`. | |
| In both cases the activation is first rounded to f16 (round to nearest even). The module reproduces that choice of | |
| path by shape. The native forward pass uses it for F16 models (SmolLM2-135M-Instruct and mindX's `mindx-gen39`, | |
| both with tied embeddings), and its attention code (`forward.rs`) uses the f16 helpers over the default f16 KV | |
| cache: `vec_dot_u16` for K cache rows, `mad` and `scale` for the V accumulator, `widen` for K and V tiles. The q8_0 | |
| KV cache (0.3.9, `BANKML_CACHE_TYPE=q8_0`) does not use them; its kernels are in `forward.rs` | |
| (`attend_head_q8`, `vec_dot_q8_0_q8_0`) and `q1_0.rs` (`quantize_row_q8_0`), see [forward.md](forward.md). | |
| ## Technical usage | |
| Constant: `F16_BYTES = 2`. | |
| ```rust | |
| pub fn to_f16(x: &[f32]) -> Vec<u16> // ggml_cpu_fp32_to_fp16 | |
| pub fn dequantize_row(row: &[u8], out: &mut [f32]) // F16 bytes widened to f32 (get_rows) | |
| pub fn widen(src: &[u16], dst: &mut [f32]) // exact; F16C where present | |
| pub fn vec_dot_ref(x: &[u8], y: &[u16]) -> f32 // ggml_vec_dot_f16, the definition | |
| pub fn sgemm_dot_ref(x: &[u8], y: &[u16]) -> f32 // one tinyBLAS element, y.len() % 8 == 0 | |
| pub fn vec_dot(x: &[u8], y: &[u16]) -> f32 // vec_dot_ref's bits, fastest path | |
| pub fn vec_dot_u16(x: &[u16], y: &[u16]) -> f32 // the same, weight row as f16 values (a K cache row) | |
| pub fn mad(acc: &mut [u16], v: &[u16], w: f32) // ggml_vec_mad_f16 (flash attention's V accumulator) | |
| pub fn scale(acc: &mut [u16], s: f32) // ggml_vec_scale_f16 | |
| pub fn takes_sgemm(rows: usize, k: usize, n: usize) -> bool | |
| ``` | |
| `x` is an F16 weight row as little-endian bytes; `y` is the f16 activation. `takes_sgemm` is | |
| `n >= 2 && rows % 4 == 0 && k % 8 == 0`. | |
| ### Matrix products | |
| ```rust | |
| pub fn mat_vec_par(pool: &crate::par::Pool, wb: &[u8], rows: usize, a: &[u16], out: &mut [f32]) | |
| pub fn mat_vec_multi_par(pool: &crate::par::Pool, ws: &[(&[u8], usize)], a: &[u16], outs: &mut [&mut [f32]]) | |
| pub fn mat_mul_par(pool: &crate::par::Pool, wb: &[u8], rows: usize, cols: &[Vec<u16>], out: &mut [f32]) | |
| ``` | |
| - `mat_vec_par`: one column, `vec_dot` per row, rows in chunks on the pool. | |
| - `mat_vec_multi_par`: several matrices by one column in one pass of the pool (Q, K and V; gate and up). Each output | |
| row is still one `vec_dot`. | |
| - `mat_mul_par`: writes `out[c·rows + r]`; `sgemm_dot` when `takes_sgemm`, else `vec_dot` per element. On AVX2 the | |
| sgemm case runs in 4-row by up-to-3-column register blocks (tinyBLAS's own blocking), with the columns widened to | |
| f32 once per product. | |
| ## How it is verified | |
| - `avx2_equals_the_scalar_models`: `vec_dot` and `mat_mul_par` against `vec_dot_ref` / `sgemm_dot_ref` for row | |
| lengths 8 to 1,536 (tails included), 1–6 columns, 12 and 13 rows (both sides of `takes_sgemm`), every bit. | |
| - `mad_and_scale_match_the_scalar_definitions`: the attention helpers, odd tails included. | |
| - `the_two_reductions_differ`: the two ggml paths give different bits for the same inputs, so the oracle has to | |
| check the path choice too. | |
| - `oracle_ggml_b11192_f16` (`#[ignore]`, needs `.models/SmolLM2-135M-Instruct-F16.gguf` and `.models/oracle-f16` from | |
| `testing/f16_oracle.py`): the shipped library computes `ggml_mul_mat` graphs on SmolLM2's real matrices and on | |
| synthetic shapes that reach the tails and fallbacks. Recorded ([oracles.md](../oracles.md) §5d): 211 of 211 tensors | |
| widened bit-exact; 552,268 of 552,268 elements in 87 products (61 tinyBLAS, 26 `vec_dot`). | |
| - End to end, the model oracles and llama-server token oracles cover the F16 models (800 of 800 rows each for | |
| SmolLM2-135M-Instruct and `mindx-gen39`). | |
| ## Design notes | |
| Exact float orders, as read from b11192 (changing any of them changes bits): | |
| - **Activation.** F16's `vec_dot_type` is F16 and its `from_float` is `ggml_cpu_fp32_to_fp16` (F16C | |
| `vcvtps2ph imm=0`, round to nearest even), so the activation is rounded to f16 before any product: `to_f16`. | |
| - **`vec_dot` (`ggml_vec_dot_f16`).** Four 8-lane f32 accumulators fed by FMAs over 32-element steps, reduced | |
| `(a0 + a2) + (a1 + a3)`, then low half + high half, then two horizontal adds; any tail (`n % 32`) is added in | |
| double. | |
| - **`sgemm_dot` (`tinyBLAS<8, __m256, __m256, ggml_fp16_t, ggml_fp16_t, float>`).** Taken when the weight has a | |
| multiple of 4 rows and the row a multiple of 8 (otherwise `llamafile_sgemm` returns false and ggml falls back to | |
| `vec_dot`). Each element is one 8-lane FMA chain over the row, `hsum`'d as | |
| `((l0 + l4) + (l2 + l6)) + ((l1 + l5) + (l3 + l7))`. | |
| - **Blocking.** `sgemm_block_avx2` follows tinyBLAS's register blocking: twelve accumulators for 4 rows × 3 columns, | |
| each step loading four weight vectors and up to three activation vectors (already widened to f32). Every element | |
| is still its own FMA chain, so the blocking does not change bits. | |
| - **Oracle.** `testing/f16_oracle.py` has the shipped library compute `mul_mat` graphs; every tensor is also widened | |
| to f32 with ggml's own conversion and compared. | |
| ## Advantages and efficiency | |
| - **Bits chosen by shape, as ggml chooses.** Because the two reductions differ, matching ggml requires the same path | |
| for the same shape. `takes_sgemm` encodes ggml's rule; the tiling and the thread split decide only who computes an | |
| element, never its bits. | |
| - **F16C and FMA on the hot path.** Weights are widened with `_mm256_cvtph_ps` inside the FMA loop. In `mat_mul_par` | |
| the activations are widened once per product, not once per weight row. | |
| - **One pool pass for grouped matrices.** `mat_vec_multi_par` hands Q, K and V (or gate and up) to the pool as one | |
| job, so the pool is woken once instead of per matrix. | |
| - **Measured** (laptop, three threads, [PERFORMANCE.md](../PERFORMANCE.md), 0.3.4): SmolLM2-135M-Instruct F16 decode | |
| 38.0–38.9 tok/s against llama-server's 40.4–42.7 (about 0.93×); an 858-token prompt in 8.6–9.6 s against | |
| 7.8–8.6 s. The steps that got there: the reference f16 dot, scale and accumulate on F16C + FMA (12.8 → 36.1 tok/s | |
| together with hash-map tensor lookup and norms read once), and tinyBLAS's 4 × 3 blocking for prompts (858-token | |
| prompt 18.5 → 14.4 s). The rest of 0.3.4's gain was attention work in `forward.rs`. Each change kept the same bits. | |
| - **Rejected with numbers.** A bounded spin before the pool's workers sleep raised `pool.run` from 15.6 µs to | |
| 162.6 µs per call on the 2-core SMT laptop and did not move decode; it was reverted. | |
| - **Practice.** `#[target_feature(enable = "avx2,fma,f16c")]` (or `"avx2,f16c"`) functions with stated safety | |
| conditions, called only after `q1_0::has_avx2()` and length checks. Reading f16 slices as bytes is guarded by a | |
| `compile_error!` on big-endian targets. | |
| ## Limitations | |
| - Decode on the F16 models is about 0.93× llama-server's (38 vs 41 tok/s on SmolLM2). The pool's condvar hand-off is | |
| the suspect; a spin was measured and rejected ([TODO.md](../TODO.md)). | |
| - x86_64 AVX2 + FMA + F16C is the only fast path; other CPUs run the scalar models (same bits). | |
| - Little-endian targets only (`compile_error!` otherwise). | |
| - F16 is the first of ggml's standard weight formats done; `Q8_0` and BF16 weight kernels are next, then `Q4_K_M` | |
| ([TODO.md](../TODO.md), O3). 0.3.9's q8_0 KV cache is not a Q8_0 weight path: a Q8_0 model is still refused. | |
| ## See also | |
| - [q1_0.md](q1_0.md) (the f16 ↔ f32 conversions and `has_avx2`), [par.md](par.md), [gguf.md](gguf.md) | |
| - [../oracles.md](../oracles.md) §5d, [../PERFORMANCE.md](../PERFORMANCE.md) (F16 and the Llama graph), | |
| [../OLLAMA.md](../OLLAMA.md) (O3, O4), [../TODO.md](../TODO.md) | |