Skip to content

[GPU] Support u2 weights in the optimized bf_tiled FC kernel - #39058

Open
Cyberpunk1210 wants to merge 11 commits into
openvinotoolkit:masterfrom
Cyberpunk1210:int2-fc
Open

Cyberpunk1210 wants to merge 11 commits into
openvinotoolkit:masterfrom
Cyberpunk1210:int2-fc

Conversation

@Cyberpunk1210

@Cyberpunk1210 Cyberpunk1210 commented Oct 21, 2026 •

Copy link
Copy Markdown

Details:

Part 2 of the split of #37335 (series: #39057 … #39063). No dependencies. The batch-1 GEMV
kernel follows in #39059.

#36170 enabled u2 FC on the reference kernel fully_connected_gpu_bfyx_ref; #37212 adds u2 FC via
oneDNN for XMX platforms. This PR enables u2 on fully_connected_gpu_bf_tiled, so a u2 FC that does
not take the oneDNN path is not left on the reference kernel. That covers two cases: devices
without XMX (or with oneDNN disabled), and u2 models with a fractional zero point. oneDNN's
weight zero point must be an integer type (i4/u4/u8/i8), but the symmetric 4-level grid
{-1.5, -0.5, +0.5, +1.5}·scale used by some 2-bit models is (q - 1.5)·scale, i.e. a scalar zp of
1.5. #37212 already declines such a layer at implementation selection (its zero-point dtype check
admits i4/u4/u8/i8 only), so the layer falls to the OCL kernels; with this PR that is bf_tiled, which
takes the zero point in accumulator precision and decodes the grid exactly, instead of the reference
kernel. (#37212's selection code notes that "u2 has no OCL fallback"; after this PR it has one.)

Relation to #38460 (TernOCL): ternocl_int2 takes u2 FCs with a scalar zero point of exactly 1
(ternary {-1, 0, +1}) on Xe2+ with DPAS, and is selected ahead of the other implementations. Every
other u2 FC — e.g. NNCF INT2_SYM (scalar zp 2), grouped zero points, fractional zero points, or
devices without DPAS — is not taken by it and lands here (and on the GEMV kernel of #39059 at
batch 1).

  • bf_tiled: non-dyn-quant tilings only, IFM a multiple of 4, no swiglu fusion, TILE_IFM 2 /
    TILE_K 4 (one weight byte per lane), os_iyx_osv16 and os_is_yx_osv64_isv2 layouts.
  • reorder_weights_int4: u2 OIYX padding, IOYX->OIYX transpose and os_iyx_osv16 repack;
    matching osv16 index in fetch_weights.cl.
  • prepare_padding aligns u2 weights to 4 values per byte; fake alignment and the
    async-compilation rule treat u2 like u4.
  • kernel_selector toCLType maps u2 to uchar (same two lines as [GPU] Support u2 compressed dictionaries in GatherCompressed #39057).
  • DynamicQuantizeFullyConnected declines u2. [GPU] Add UINT2 (2-bit) weight compression support via oneDNN fully_connected  #37212 adds the same rule in the pipeline
    callback
    ; whichever lands second should drop one of them.

Tests: bf_tiled u2 FC over batch / IFM / OFM sizes and scale group sizes, with and without a grouped
zero point, plus an os_is_yx_osv64_isv2 case; u2 reorder_data cases.

Tickets:

  • N/A

AI Assistance:

🤖 Generated with Claude Code

PR openvinotoolkit#36170 enabled u2 on FullyConnected_bfyx_Ref, the one-work-item-per-output
reference kernel. This enables it on bf_tiled, the optimized OCL path, so a u2
FC on a device that does not take the oneDNN path (no XMX, or oneDNN disabled)
is not left on the reference kernel.

u2 is not dynamic-quantizable, so it takes the non-dyn-quant tilings, with
constraints that all come from 4 values sharing one byte:
  - Validate: IFM must be a multiple of 4, and swiglu fusion is excluded (it
    needs os_is_yx_osv32_isv2, which cannot be produced for u2).
  - VerifyTuneParams: only TILE_IFM 2 / TILE_K 4, whose per-lane weight
    staging reads exactly one byte, matching both u2 packings; SLM only when
    IFM divides evenly so no partial byte is left over.
  - Jit: tile_k_ofm_packed /= 4; FILTER_ELEMENTS_PER_BYTE is a jit constant,
    not a .cl #define, because kernels are concatenated into one batched
    program and only jit constants get an automatic #undef.
  - Layouts: os_iyx_osv16 and os_is_yx_osv64_isv2.

Supporting changes:
  - reorder_weights_int4 gains u2 variants of its three layout paths (OIYX
    padding, IOYX->OIYX transpose, os_iyx_osv16 repack), gathering 4 values per
    output byte and zero-filling the padded tail; fetch_weights.cl gets the
    matching osv16 index for 4-values-per-byte packing.
  - prepare_padding aligns u2 weights to 4 values per byte instead of 2;
    fake alignment and the async-compilation rule treat u2 like u4.
  - kernel_selector toCLType maps u2 to uchar, like u4.
  - DynamicQuantizeFullyConnected declines u2: no kernel implements i8
    activations x u2 weights. openvinotoolkit#37212 adds the same rule in the pipeline
    callback; whichever lands second should drop one of them.

Tests: bf_tiled u2 FC over batch / IFM / OFM sizes and scale group sizes, with
and without a grouped zero point, plus an os_is_yx_osv64_isv2 case; u2
reorder_data cases (identity, u2->f16, f16->u2 clamping and partial bytes).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Cyberpunk1210 <genji.1231@outlook.com>
Signed-off-by: Cyberpunk1210 <genji.1231@outlook.com>

# Conflicts:
#	src/plugins/intel_gpu/tests/unit/test_cases/fully_connected_gpu_test.cpp
Cyberpunk1210 added a commit to Cyberpunk1210/openvino that referenced this pull request Oct 21, 2026
Merges current master (via the updated openvinotoolkit#39058/openvinotoolkit#39059 branches) so the combined branch stays
mergeable; content is still exactly the seven split PRs.

Signed-off-by: Cyberpunk1210 <genji.1231@outlook.com>
@p-durandin

Copy link
Copy Markdown
Contributor

build_jenkins

Comment-only cleanup in the u2 bf_tiled / reorder_weights_int4 changes and their
tests (keep the why, drop restated code and history), plus the toCLType case
order requested in the GatherCompressed review.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Cyberpunk1210 <genji.1231@outlook.com>
@p-durandin

Copy link
Copy Markdown
Contributor

build_jenkins

Return the conditions directly (clang-tidy readability-simplify-boolean-expr). No behaviour change.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Cyberpunk1210 <genji.1231@outlook.com>
@p-durandin

Copy link
Copy Markdown
Contributor

build_jenkins

@v-Golubev v-Golubev self-assigned this Oct 22, 2026
// #undef'd between kernels of a batched program.
jit.AddConstant(MakeJitConstant("FILTER_ELEMENTS_PER_BYTE", weights_dt == WeightsType::UINT2 ? 4 : 2));
if (weights_dt == WeightsType::UINT2) {
jit.AddConstant(MakeJitConstant("COMPRESSED_WEIGHTS_INT4", 1));

@michal-miotk michal-miotk Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: probably better not mix this name COMPRESSED_WEIGHTS_INT4 into u2 path

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done: bf_tiled now uses its own COMPRESSED_WEIGHTS_SUB_BYTE (i4/u4/u2) for the shared packed-weight paths, and u2 no longer sets COMPRESSED_WEIGHTS_INT4.

u2 no longer sets COMPRESSED_WEIGHTS_INT4; bf_tiled defines its own macro for i4/u4/u2 and its kernels switch to it. Rename only.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Cyberpunk1210 <genji.1231@outlook.com>
@p-durandin

Copy link
Copy Markdown
Contributor

build_jenkins

const uint h = output_index / OUTPUT_INNERMOST_NUM;
if (w < INPUT0_INNERMOST_NUM) {
const uint in_elem_offset = h * INPUT0_INNERMOST_NUM + w;
out |= ((input[in_elem_offset / 4] >> ((in_elem_offset % 4) * 2)) & 0x3) << (j * 2);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: please split out | expression to more vars

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done: split into in_shift / val locals.

…order expression

Move the "no FC kernel takes i8 activations with u2 weights" check out of the
DynamicQuantizeFullyConnected pass body into its pass_config callback, so the
pass itself matches upstream and DynamicQuantizeFullyConnectedTests (u2 case)
passes; plugin behaviour is unchanged. Also split the packed-u2 reorder
expression in reorder_weights_int4.cl into named locals.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Cyberpunk1210 <genji.1231@outlook.com>
@github-actions github-actions Bot removed the category: transformations OpenVINO Runtime library - Transformations label Oct 27, 2026
…ts_int4

Use in_shift/val locals instead of the compound bit expression in the IOYX->OIYX branch.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Cyberpunk1210 <genji.1231@outlook.com>
@v-Golubev

Copy link
Copy Markdown
Contributor

build_jenkins

auto weights_layout_dt = orig_impl_param.weights_layout.value().data_type;
auto is_4bit = weights_layout_dt == data_types::i4 || weights_layout_dt == data_types::u4;
auto is_4bit = weights_layout_dt == data_types::i4 || weights_layout_dt == data_types::u4 ||
weights_layout_dt == data_types::u2;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: please ad is_2bit

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done: added is_2bit for u2; is_4bit is back to i4/u4.

#elif FILTER_LAYOUT_OS_IS_YX_OSV64_ISV2
SLM_FILTER_PACKED_VEC wei_packed0 = BLOCK_READN(FILTER_TYPE, FILTER_ACTUAL_LOAD_BLOCK_SIZE, weights, weights_idx);
#if COMPRESSED_WEIGHTS_UINT2
SLM_FILTER_PACKED_VEC wei_packed1 = BLOCK_READN(FILTER_TYPE, FILTER_ACTUAL_LOAD_BLOCK_SIZE, weights, (weights_idx + (FILTER_LOAD_BLOCK_SIZE * SIMD / 2)));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please change this line to: SLM_FILTER_PACKED_VEC wei_packed1 = BLOCK_READN(FILTER_TYPE, FILTER_ACTUAL_LOAD_BLOCK_SIZE, weights, (weights_idx + U2_OSV64_ISV2_LINE_SIZE_BYTES)); where U2_OSV64_ISV2_LINE_SIZE_BYTES is 32

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done: the read now uses U2_OSV64_ISV2_LINE_SIZE_BYTES (32), defined at the top of the kernel.

@michal-miotk

Copy link
Copy Markdown
Contributor

please add SLM test

…ests

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Cyberpunk1210 <genji.1231@outlook.com>
@Cyberpunk1210

Copy link
Copy Markdown
Author

Added u2 bf_tiled SLM tests: fully_connected_gpu_u2_bf_tiled_slm.osv16 (batch 256 and 512 with zp) and fully_connected_gpu_u2_bf_tiled_osv64.horizontal_weights_slm (batch 256). They check the selected kernel JIT has USE_SLM 1 on integrated GPUs.

prepare_padding.cpp conflicted with openvinotoolkit#39073; take upstream's version. The u2 change there
only touched the oneDNN branch, and oneDNN FC does not take u2 weights, so it was never reached.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Cyberpunk1210 <genji.1231@outlook.com>
@v-Golubev

Copy link
Copy Markdown
Contributor

build_jenkins

#endif

#if COMPRESSED_WEIGHTS_INT4
#if COMPRESSED_WEIGHTS_SUB_BYTE

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like this branch is not touched by u2 - please express that in #if

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, added && COMPRESSED_WEIGHTS_INT4.

@@ -55,7 +86,23 @@ KERNEL(reorder_weights_int4)(const __global INPUT0_TYPE* input, __global OUTPUT_

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[random spot][nit] please change filename to sth like ....compressed... to not contain int4 explicitly

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, renamed to reorder_weights_compressed.

… the osv32 path int4-only

The kernel now also repacks u2, so the int4 name is misleading. The osv32_isv2
branch of the bf_tiled FC kernel is never taken for u2; say so in the #if.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Cyberpunk1210 <genji.1231@outlook.com>
@Cyberpunk1210
Cyberpunk1210 requested a review from a team as a code owner October 29, 2026 06:36

if (root->get_input_element_type(1) == ov::element::u2) {
GPU_DEBUG_TRACE << root->get_friendly_name() << " dyn_quan is turned off:"
" no FC kernel takes i8 activations with u2 weights" << std::endl;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please change comment to be more broad - not contain any compressed activation not only i8

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, reworded to "compressed activations".

@v-Golubev

Copy link
Copy Markdown
Contributor

build_jenkins

@michal-miotk

Copy link
Copy Markdown
Contributor

overall LGTM

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Cyberpunk1210 <genji.1231@outlook.com>
@v-Golubev

Copy link
Copy Markdown
Contributor

build_jenkins

@v-Golubev v-Golubev left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A question on the overall direction. oneDNN already supports u2 FC (#37212): it uses plain weights layouts, so no repacking to os_iyx_osv16 / os_is_yx_osv64_isv2 and no extra reorder kernels. This path looks preferable, since it means much less code to maintain on the OpenVINO side and it avoids extra memory consumption.

Could you please clarify which code paths this PR closes that oneDNN wouldn't? Do we have real workloads (models/devices) that hit them?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

category: GPU OpenVINO GPU plugin ExternalPR External contributor

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants