改动 / #3472

#3472 [Engine] Zen 1/2: build ggml-cpu without BMI2 pdep (IQ1_S/IQ1_M CPU dots 16-19x faster, same results)

open · @Licenser · 0 评论 · 去 GitHub 看

needs-format

说明

## Problem and result

On AMD Zen 1, Zen+ and Zen 2 (CPU family 17h, and Hygon's 18h) the BMI2 instruction `pdep` runs in microcode: on the
order of a hundred cycles or more, against a few on Intel and Zen 3+. ggml-cpu's AVX2 IQ1_S and IQ1_M dot products
build their grid indices with `pdep` whenever `__BMI2__` is defined, which every `-march=native` build on such a CPU
does. There those two dots run 10-19x slower than the `#else` branch beside them, which computes the same integer
sums with shifts and masks. Upstream GSQ-RCO IQ2_XS has IQ1_M gate/up experts in 3 of its 48 layers (8, 13, 37), so
on a Zen 2 PC those three layers take most of the CPU pool's time.

Measured on a Threadripper 3960X (Zen 2) + RTX 2080 Ti 11 GB, Linux, GCC 15.2, upstream IQ2_XS:

| | with pdep | without |
|---|---|---|
| `ggml_dot_bench`, IQ1_S, ns per 256 weights (one thread) | 379 | 24 |
| `ggml_dot_bench`, IQ1_M, ns per 256 weights | 543 | 29 |
| `kq_cpu_bench`, one IQ1_M expert, ms per thread (24 threads, 1 / 2 tokens) | 7.53 / 14.99 | 0.72 / 1.42 |
| engine, CPU time per CPU expert | 46-51 us | 21-23 us |
| engine, decode tok/s (4 requests) | 58-76 | 71-99 |
| engine, short prompts tok/s | 58-70 | 94-103 |
| engine, 6.3K-token prompt tok/s (runs on the GPU) | 1104 | 1115 |

The output does not change: both builds compute byte-identical results (Validation).

After this change, a native build on a Zen 1/2 build machine compiles ggml-cpu without `__BMI2__`, so the two dots
take their existing `#else` branch. Nothing changes anywhere else.

## Related discussion

None. This only changes what a native build compiles on Zen 1/2 build machines, and the results stay identical, so I
treated it as a focused performance fix. Happy to open an issue first if you would rather discuss the default.

## Approach

A new CMake option, `STRATA_GGML_PDEP` (`AUTO` / `ON` / `OFF`, default `AUTO`):
- `AUTO`: in a native build (not `STRATA_PORTABLE`, no `STRATA_ISA_FLOOR`) whose build machine is AMD family 17h or
  Hygon 18h (`/proc/cpuinfo`, or `PROCESSOR_IDENTIFIER` on Windows), sets `GGML_BMI2` off and adds `-U__BMI2__` to
  ggml-cpu (GCC and Clang: `-march=native` defines `__BMI2__` whatever `GGML_BMI2` says). CMake prints a status line
  when this applies.
- `OFF`: no `pdep` in any build. `ON`: never removed.
- Unchanged: the portable (release) build, which also runs on CPUs where `pdep` is fast; every other CPU; MSVC builds,
  which define `__BMI2__` only through `GGML_BMI2` (set only by the release build), so they already used the `#else`
  branch.

No ggml source is touched; the `#else` branches are ggml's own code. Besides `ggml_cpu_has_bmi2()` (a report), these
two dots are ggml's only `__BMI2__` code.

Also adds `ggml_dot_bench` (ggml-cpu's dot product per format, with a checksum of the results, CPU only, no model)
and `tools/bench_ggml_pdep.py`, which builds it with and without `pdep` and prints both side by side, so other PCs
can measure. `docs/DETAILS.md` documents the option.

## Validation

**Passed:**
- `python tools/bench_ggml_pdep.py --passes 2` (this commit; 3960X, Linux, GCC 15.2): IQ1_S 379.3 -> 23.8 ns,
  IQ1_M 543.0 -> 28.6 ns per 256 weights. The 9 other formats (IQ2_XXS, IQ2_XS, IQ2_S, IQ3_XXS, IQ3_S, IQ4_NL,
  IQ4_XS, Q2_K, Q4_K) stay within 0.93-1.08x, and the checksums of all 11 formats are equal between the two builds.
- `objdump` of the engine built from this commit (CUDA sm_75, `AUTO` on the 3960X): 0 `pdep`. Its two IQ1 dot
  functions are instruction-for-instruction those of the `-U__BMI2__` engine used for the measurements below.
- `kq_cpu_bench --pairs iq1_m/q2_0,iq2_s/q2_0 --threads 1,24 --nt 1,2`: iq1_m/q2_0 7.53 -> 0.72 ms per expert per
  thread (24 threads, 1 token), 14.99 -> 1.42 (2 tokens). iq2_s/q2_0, which has no `pdep`: 0.554 vs 0.571 ms.
- `tools/ab_engine.py`, 4 alternating rounds, upstream IQ2_XS, `--expert-cache auto`, `--spec 4` with the draft
  layer, `STRATA_DECODE_TIMING=1`: the engine rows of the table above (medians per request). Engines built from
  477ac13c plus local changes that are off by default, once as is and once with
  `-DCMAKE_C_FLAGS=-U__BMI2__ -DCMAKE_CXX_FLAGS=-U__BMI2__`, which gives the same object code as this option
  (see `objdump` above).
- `tools/quant_quality.py run` with a fixed placement (every expert on the CPU, no adaptation, so layers 8, 13 and
  37 run the IQ1_M dot): the two builds' outputs (4,995 positions: the target's logp, the argmax and the 64 most
  likely tokens each) are byte-identical (sha256 245adafe...).
- The engine with the draft window sizes held fixed (a local test switch, not part of this PR): both builds gave the
  same tokens and the same draft and cache counters; with that same work, decode was 19-31% faster and short prompts
  48-60%.
- Builds: CUDA (sm_75) and CPU-only, Linux, GCC 15.2, CUDA 13.4.

**Failed:**
- None observed.

**Not run:**
- Zen 1 / Zen+ and Hygon hardware: detected by CPU family, where `pdep` is also microcoded, but not measured.
- Intel and Zen 3+: `AUTO` does not apply there. `tools/bench_ggml_pdep.py` should show both builds close.
- The release (portable) build on Zen 2: unchanged by this PR.
- HIP, SYCL and Metal builds; Windows (MSVC) builds, including the `PROCESSOR_IDENTIFIER` branch.

## Limits

- The release build keeps `pdep`, so Zen 1/2 users of the release binaries still pay for it. Fixing that needs a
  runtime choice in ggml, which is out of scope here.
- The choice follows the build machine: a native build copied to another CPU keeps it.
- Only packs with IQ1_S / IQ1_M layers change speed (upstream IQ2_XS: 3 of 48 layers; the IQ3_S, IQ3_XXS and Q2_0
  packs have none).
- One machine measured.
- In a normal engine A/B the two builds' answers can differ although the arithmetic is identical: the draft policy
  prices window sizes from measured round times, so a faster CPU picks other windows. With the windows held fixed
  they are the same.

Disclaimer: I'm not a C++ dev / CMake is not a usual tool for me, this used Claude and I reviewed to the best of my ability

本站相关内容

相关页面的快捷入口。