Skip to content

vulkan : fix out-of-bounds NVFP4 UE4M3 scale lookup - #31079

Merged
0cc4m merged 2 commits into
ggml-org:masterfrom
flyingdoog:vulkan-nvfp4-ue4m3-lut
Nov 3, 2026
Merged

0cc4m merged 2 commits into
ggml-org:masterfrom
flyingdoog:vulkan-nvfp4-ue4m3-lut

Conversation

@flyingdoog

@flyingdoog flyingdoog commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

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

  • I have read and agree with the contributing guidelines
  • AI usage disclosure: YES - patch and reproducer generation; code review and submission tooling.

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
@github-actions github-actions Bot added Vulkan Issues specific to the Vulkan backend ggml changes relating to the ggml tensor library for machine learning labels Oct 24, 2026
@flyingdoog
flyingdoog marked this pull request as ready for review October 24, 2026 07:34
@flyingdoog
flyingdoog requested a review from a team as a code owner October 24, 2026 07:34
@flyingdoog
flyingdoog marked this pull request as draft October 24, 2026 08:04
@flyingdoog
flyingdoog marked this pull request as ready for review October 24, 2026 08:11
@ggarembroo

Copy link
Copy Markdown

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?

@jeffbolznv

Copy link
Copy Markdown
Contributor

Do any models actually set this high bit? Do other backends handle it?

@flyingdoog

flyingdoog commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor Author

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.

  1. CPU, SYCL, and WebGPU ignore it natively. The Vulkan LUT path fixed in this PR also ignores it.
  2. CUDA FP8, Vulkan OCP, and HIP CDNA3 treat it as a sign bit.
  3. Blackwell native leaves it undefined.

I only tested CUDA FP8 and Vulkan LUT. The others is from code/documents

@flyingdoog

flyingdoog commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor Author

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.

  1. CPU, SYCL, and WebGPU ignore it natively. The Vulkan LUT path fixed in this PR also ignores it.
  2. CUDA FP8, Vulkan OCP, and HIP CDNA3 treat it as a sign bit.
  3. Blackwell native leaves it undefined.

I only tested CUDA FP8 and Vulkan LUT. The others is from code/documents

@ggarembroo

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.

@0cc4m
0cc4m merged commit 37b0f9c into ggml-org:master Nov 3, 2026
26 of 29 checks passed
edwardyoon pushed a commit to edwardyoon/focus-llama that referenced this pull request Nov 6, 2026
* 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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ggml changes relating to the ggml tensor library for machine learning Vulkan Issues specific to the Vulkan backend

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants