Skip to content

ggml : support F32 input for int8 convrot tensorwise mulmat in Vulkan - #8

Open
noctrex wants to merge 4 commits into
leejet:int8_convrotfrom
noctrex:vulkan-f32-input
Open

ggml : support F32 input for int8 convrot tensorwise mulmat in Vulkan#8
noctrex wants to merge 4 commits into
leejet:int8_convrotfrom
noctrex:vulkan-f32-input

Conversation

@noctrex

@noctrex noctrex commented Aug 27, 2026

Copy link
Copy Markdown

The known limitation from #7: mul_mat_i8_tensorwise accepts src1 as either the packed QUANTIZE_I8_CONVROT output or raw F32 (CUDA/HIP quantize inline), but Vulkan only handled the packed form, so F32 inputs were rejected in supports_op and those graphs fell back to CPU via the scheduler.
Consumers such as stable-diffusion.cpp emit the F32 form for Linears with a folded activation scale, which made the Vulkan path fall back on real models.
This closes that gap for group sizes 64 and 256.

Implementation (7a91b8f):

  • ggml_vk_can_use_mul_mat_i8_tensorwise now accepts both input forms. The F32 form additionally requires the packed staging size (K * rows_padded + row scales) to fit maxStorageBufferRange.
  • ggml_vk_mul_mat_i8_tensorwise quantizes the F32 input on the fly into a new grow-on-demand device prealloc buffer (prealloc_qconvrot) laid out exactly like the packed tensor (payload + trailing row scales), using the existing quantize_i8_convrot pipelines selected by the op-param group size. The existing tensorwise GEMM then runs unchanged on that buffer.
  • The packed (pre-quantized) path is untouched; the prealloc buffer follows the same resize + need_sync discipline as prealloc_x/prealloc_y.
  • Cost: one extra device-local prealloc buffer, reused across nodes and steps (~83 MB worst case at K=10240, 8108 rows for the Z-Image shapes).

Tests: tests/test-int8-convrot.cpp gained F32-input cases - GPU packed-vs-F32 equivalence plus an exact CPU reference in CPU mode.

Verification (RX 7900 XTX, Windows, Adrenalin, KHR_coopmat + integer dot):

  • test-int8-convrot-gpu: passes in Vulkan mode and in CPU-exact mode.
  • No-regression: test-int8-convrot-h64 and the packed-path probe produce byte-identical baseline signatures versus the pre-change build.
  • Real-shape equivalence (Z-Image/Krea2 K=3840/10240, N up to 11520, up to 8108 rows): out_f32 == out_packed bit-identical on Vulkan, 5 cases, 218M output elements, 0 mismatches. The prealloc resize path was exercised (buffer growth mid-process, then reuse).
  • End to end: Z-Image turbo H64, 1440x1440. Before: every folded-scale Linear logged "unsupported node ... MUL_MAT src1=f32" and ran on CPU via the scheduler. After: zero unsupported nodes, diffusion fully on GPU, image output correct.

Remaining gap (unchanged, smaller scope): convrot_group_size 0 (plain rowwise) F32 input still falls back to CPU - there is no group-0 Vulkan quantize shader.

Not yet verified: NVIDIA hardware, Linux.

Related: #6, #7, ggml-org/llama.cpp#26914, leejet/stable-diffusion.cpp#1857.

LLM Disclose: YES, used GLM-5.3 for the grunt work

Remove the blanket GGML_USE_HIP exclusion from the INT8 tensorwise
convrot ops and scope the capability check to GPUs with working
integer GEMM support (turing_mma_available, amd_wmma_available,
amd_mfma_available). RDNA2 stays excluded: hipBLASLt INT8 is broken
there.

vendors/hip.h: map CUDA_R_8I, CUDA_R_32I, CUBLAS_COMPUTE_32I and
__shfl_down_sync to their HIP equivalents. HIP's masked shuffle
templates require a 64-bit mask, so route to the maskless intrinsic
like the other __shfl_*_sync mappings.

Verified on gfx1100 (Windows ROCm 7.1): ggml tests/test-int8-convrot
passes, and CPU-vs-GPU output is bit-exact at real model shapes
(K=3840/10240, N up to 11520, 8104 rows).
Template the INT8 convrot quantization kernels on group_size and
instantiate 64 alongside 256. The Hadamard normalization is now
derived from the group size (2^-log4(group_size): 1/8 for G=64,
1/16 for G=256). The small-kernel max reduction uses a shared-memory
tree instead of warp shuffles so it stays correct for 16-thread
blocks. Capability gates accept group size 64 and 256.

Z-Image int8_tensorwise exports in the wild use convrot_groupsize
64; without this they fall back to CPU on every GPU backend.

Verified on gfx1100 (Windows ROCm 7.1): exact-reference tests pass
for H64 (single-group and multi-group packed GEMM), H256 unchanged,
and CPU-vs-GPU output is bit-exact at real Z-Image shapes.
Parameterize the quantize shader with a GROUP_SIZE macro (default 256,
validated {64, 256}), register the G64 SPIR-V variant, relax the quantize
and tensorwise mulmat gates to accept group size 64, and select the
pipeline by the op param group size. The GEMM shaders are group-size
agnostic and unchanged. The G256 variant compiles to byte-identical
SPIR-V as before.
The tensorwise mulmat accepts src1 as either the packed
QUANTIZE_I8_CONVROT output or raw F32, but the Vulkan backend only
handled the packed form, so F32 inputs fell back to the CPU via the
scheduler.

Relax the supports_op gate to accept the F32 form, bounded by
maxStorageBufferRange for the packed staging size. Quantize the F32
input on the fly into a grow-on-demand device prealloc buffer in the
packed payload+scales layout, then run the existing tensorwise GEMM
unchanged. Extend the Vulkan test with F32-input cases (GPU packed
equivalence, exact CPU reference).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant