Repository navigation
vulkan : fix out-of-bounds NVFP4 UE4M3 scale lookup - #31079
Conversation
On the non-OCP path, ue4m3_fp32_lut has 128 entries and is indexed with the raw scale byte. ggml_validate_row_data accepts every byte and ggml_ue4m3_to_fp32 ignores bit 7, so a byte >= 0x80 reads past the shared array. Vulkan robustness does not cover shared memory. Size the table, both init loops, and the two host shared-memory budgets to 256 entries. ue4m3_to_fp32_build() already masks the exponent and treats only 0 and 0x7F as special, so entries 128-255 match the CPU. A 256-entry table keeps 0xFF distinct from 0x7F: the CPU decodes 0xFF as a non-zero value and 0x7F as zero. Each NVFP4 pipeline uses 512 more bytes of shared memory. ggml_fp32_to_ue4m3 never sets bit 7, so models quantized by llama.cpp are unaffected. Assisted-by: Claude Assisted-by: Codex
Assisted-by: Codex
|
The 256-entry LUT fixes the OOB for bytes >= 0x80, but it raises shared memory for every NVFP4 pipeline. Can you confirm both updated budgets cover all NVFP4 shader paths, and add a regression case with a 0xFF scale byte comparing CPU and Vulkan output? |
|
Do any models actually set this high bit? Do other backends handle it? |
Currently, I think the high bit cannot be produced via a normal way. llama.cpp's conversion and quantization do not produce it from the start. Upstream marked it invalid in #27512 , but only in TODO comments; For other backends,I use AI to help summarize for your information.
I only tested CUDA FP8 and Vulkan LUT. The others is from code/documents |
The two host-side checks (scalar/coopmat1 matmul and coopmat1 int matmul) both account for the 256-entry table. The other NVFP4 paths (coopmat2, mat-vec, dequant/get_rows) have no budget check before or after this PR. Their worst case after this PR is 17424 B, which comes from the 128-thread mat-vec workgroup that is only selected on NVIDIA/Intel, and those report >= 32 KiB. The one exception is Imagination's proprietary driver (subgroup 128, no subgroup ops, 16 KiB limit), where the 8-column mat-vec already used 16912 B before this PR, so that overflow predates this change. I also cross-checked the maxComputeSharedMemorySize values reported on vulkan.gpuinfo.org, and no device limit falls in the 512-byte window between old and new usage, so nothing starts overflowing because of this PR. I written and verified the the regression test. It passes 5/5 on this PR (full nvfp4 suite 336/336) and hangs or returns NMSE=inf on the base commit without the fix. But I'm holding off pushing it for now, since test-backend-ops compares every backend against the CPU, and it fails 4/4 on CUDA FP8 builds, which treat bit 7 as a sign bit. Given the upstream TODO now calls these bytes invalid, the right test might be "no OOB read / no hang". I can update whichever version once the bit-7 semantics are settled. |
* vulkan : fix out-of-bounds NVFP4 UE4M3 scale lookup On the non-OCP path, ue4m3_fp32_lut has 128 entries and is indexed with the raw scale byte. ggml_validate_row_data accepts every byte and ggml_ue4m3_to_fp32 ignores bit 7, so a byte >= 0x80 reads past the shared array. Vulkan robustness does not cover shared memory. Size the table, both init loops, and the two host shared-memory budgets to 256 entries. ue4m3_to_fp32_build() already masks the exponent and treats only 0 and 0x7F as special, so entries 128-255 match the CPU. A 256-entry table keeps 0xFF distinct from 0x7F: the CPU decodes 0xFF as a non-zero value and 0x7F as zero. Each NVFP4 pipeline uses 512 more bytes of shared memory. ggml_fp32_to_ue4m3 never sets bit 7, so models quantized by llama.cpp are unaffected. Assisted-by: Claude Assisted-by: Codex * vulkan : fix out-of-bounds NVFP4 UE4M3 scale lookup Assisted-by: Codex --------- Co-authored-by: Dongsheng Luo <7137760+flyingdoog@users.noreply.github.com> (cherry picked from commit 37b0f9c)
Overview
On the non-OCP path, ue4m3_fp32_lut has 128 entries and is indexed with the raw scale byte. ggml_validate_row_data accepts every byte, and ggml_ue4m3_to_fp32 handles the full byte range. The CPU decoder excludes bit 7 from exponent and mantissa extraction, but compares the full byte when checking for the special values 0 and 0x7F. A scale byte >= 0x80 therefore indexes past the shared array on the Vulkan path. Vulkan robustness does not cover shared memory.
Size the table, both init loops, and the two host shared-memory budgets to 256 entries. ue4m3_to_fp32_build() already masks the exponent and treats only 0 and 0x7F as special, so entries 128-255 match the CPU decoding after accounting for the Vulkan path's subsequent factor of 0.5. A 256-entry table keeps 0xFF distinct from 0x7F: the CPU decodes 0xFF as a non-zero value and 0x7F as zero.
Each workgroup in a non-OCP NVFP4 shader that uses this table requires 512 more bytes of shared memory. ggml_fp32_to_ue4m3 never sets bit 7, so models quantized by llama.cpp do not trigger this out-of-bounds lookup.
Requirements