Skip to content

Target-wide -mavx512* puts EVEX in the scalar dequant fallbacks; source builds SIGILL on non-AVX-512 x86_64 #2021

Description

@pjordanandrsn

The AVX-512 build flags are scoped two different ways:

  • Enablement is target-wide: CMakeLists.txt:375 gates on whether the compiler accepts -mavx512f,
    true on any modern GCC regardless of the machine, then applies -mavx512f -mavx512bw -mavx512dq -mavx512vl to the whole bitsandbytes target.
  • Suppression is per-function: the #pragma GCC target("avx2,fma,no-avx512f") region from Enable CPU Optimizer Support for bitsandbytes #1901
    covers the scalar quantize functions that PR added, and nothing else.

GCC can then auto-vectorize any ungated function in those TUs with EVEX instructions. That includes the
paths which exist because the CPU has no AVX-512. dequantizeBlockwise8bitCpu has no AVX-512 branch at
all, and dequantizeBlockwise4bitCpu's scalar fallback is the branch reached when has_avx512f() returns
false. Both get compiled with AVX-512 anyway, so a from-source build on a machine without it dies the
first time one of them runs.

Runtime detection reports correctly here (has_avx512bf16()0, backends.cpu.ops._has_avx512
False). It cannot guard a function that was never an AVX-512 path to begin with.

Repro

Xeon W-1250 (Comet Lake, no AVX-512) / g++ (Debian) 14.2.0 / torch 2.13.0+cpu / Python 3.10.20, built
with RUNNER_OS=Linux RUNNER_ARCH=X64 bash .github/scripts/build-cpu.sh:

import torch, bitsandbytes as bnb

A = torch.randn(64, 64, dtype=torch.float16, requires_grad=True)
B = torch.randn(96, 64, dtype=torch.float16)
torch.nn.init.xavier_uniform_(B)
bias = torch.randn(96, dtype=torch.float16, requires_grad=True)

B2, qs = bnb.functional.quantize_4bit(B, compress_statistics=True, quant_type="fp4")
out = bnb.matmul_4bit(A, B2, qs, bias=bias)
# Illegal instruction (exit 132)

compress_statistics=True is crucial; it routes the nested absmax through the 8-bit blockwise
dequant. Same crash from the suite via
tests/test_autograd.py::test_matmul_4bit[quant_type=fp4-compress_statistics=T-fp16-has_bias=T-transpose_B=T-req_grad=TFT-dim4=96-dim3=64-dim2=64-cpu].

Under gdb:

Program received signal SIGILL, Illegal instruction.
=> _Z26dequantizeBlockwise8bitCpuIfEvPfPhPKfPT_xx._omp_fn.0+415:
       vinsertf32x4 $0x1,%xmm1,%ymm3,%ymm3
#0  dequantizeBlockwise8bitCpu<float>(...) [clone ._omp_fn.0]
#1  GOMP_parallel ()
#2  cdequantize_blockwise_cpu_fp32 ()      # ctypes, from backends/cpu/ops.py:101

vinsertf32x4 is AVX-512F, but the operands here are ymm/xmm. No zmm, no {%k} mask. So
-mprefer-vector-width=256 doesn't prevent it, and a disassembly grep for %zmm/{%k} misses it
outright; that pattern scored the faulting function as clean on my first pass. The reliable check counts
instructions whose encoding starts with the EVEX prefix 0x62:
objdump -d libbitsandbytes_cpu.so | grep -c $'\t62 '.

Measured that way, the shipped configuration emits 1264 EVEX instructions inside the intentional gated
kernels (gemv_4bit_inference, tinygemm_kernel_nn) and 130 outside them, spread across
dequantizeBlockwise8bitCpu<float,bf16_t>._omp_fn.0,
dequantizeBlockwise4bitCpu<float,fp16_t,bf16_t>._omp_fn.0, and the cdequantize_blockwise_cpu_*
wrappers. Forcing HAS_AVX512F_FLAG off takes both counts to zero and the repro passes.

Repairs that don't work

  • Extending Enable CPU Optimizer Support for bitsandbytes #1901's push_options region upward over the dequantize kernels. No change: still 130, still
    SIGILL. These are templates, and their explicit instantiations at cpu_ops.cpp:922–950 sit outside the
    region, so codegen happens there under the target-wide options. That probably also limits the existing
    Zen3 mitigation wherever a guarded template is instantiated outside its region.
  • __attribute__((target("avx2,fma,no-avx512f"))) on the definitions. Also no change, byte for byte.
    cpu_ops.h:324–332 declares both templates without the attribute, and adding it only at the definition
    doesn't take.
  • Neither touches cdequantize_blockwise_cpu_*, which lives in pythonInterface.cpp:766, a second TU the
    same flags cover.

Annotation-based scoping would have to be applied across header declarations, definitions and both TUs,
then survive OpenMP outlining on top of that. The ._omp_fn.0 clones hold most of the leaked instructions.

Suggested fix

Compile the AVX-512 kernels in their own translation unit with the -mavx512* flags, via
set_source_files_properties(... COMPILE_OPTIONS ...) or a small object library, and leave the rest at
-mavx2. That avoids depending on attributes propagating correctly through template instantiation and OMP
outlining. Failing that, a -DBNB_ENABLE_AVX512=OFF opt-out would give affected users a supported
source-build path; I verified that configuration builds clean, emits zero EVEX and passes.

The released wheel is not reproducibly affected. bitsandbytes==0.50.0 passes this test on the same
machine, and I could not fault it across 84 quantize_4bit/dequantize_4bit combinations (bf16/fp32/fp16
× nf4/fp4 × blocksize 64–4096 × plain and nested). Its 8-bit dequant carries no EVEX. It does carry 20
ungated EVEX instructions in dequantizeBlockwise4bitCpu<bf16_t/float>._omp_fn.0 which I could not reach,
so I claim no user impact there, but the shipped artifact's safety currently rests on the release
compiler's vectorization choices rather than on anything structural. This is also one machine; I have no
second non-AVX-512 host to confirm on.

CI being green is consistent with that. #1901 hit this on a non-AVX-512 runner back in March, and the
runners' compiler evidently doesn't vectorize these particular loops to EVEX. The exposure is
compiler-version-dependent rather than closed. A build-time assertion that no EVEX appears outside the
gated kernels would catch the class cheaply.

Found while running the CPU test matrix locally against post-0.50.0 main. Happy to send the PR for
either fix, though the file split touches enough of cpu_ops.cpp that I'd want your preference on layout
first. I have no AVX-512 hardware, so someone would need to check the gated kernels still build and
perform on that side.

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions