Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions ggml/src/ggml-openvino/ggml-openvino-extra.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ void ggml_openvino_device_config::init() {
"GGML_OPENVINO_RELEASE_WEIGHTS",
"GGML_OPENVINO_REDUCE_COMPILE_MEM",
"GGML_OPENVINO_COMPILED_MODEL_CACHE_DIR",
"GGML_OPENVINO_REQUANT_KQUANT",
};

for (const char * const & env_var : env_var_names) {
Expand Down Expand Up @@ -253,9 +254,66 @@ std::optional<ExtraQuantType> ggml_openvino_get_requant_type(const ggml_tensor *
if (ggml_openvino_is_npu()) {
return ExtraQuantType::Q4_0_128;
}
// By default Q6_K/Q5_K are requantized to Q8_0_C, which *inflates* 6- and 5-bit weights to 8
// while the rest of the model stays at 4 bits, and Q4_K keeps its native group-32 layout
// (an f16 scale plus an f16 zero point per 32 weights = 0.125 B/weight of metadata).
// Decode of a large model is bandwidth-bound, so both cost throughput.
//
// GGML_OPENVINO_REQUANT_KQUANT selects a 4-bit target instead. Names are
// q4_<sym|asym><group>[_all]: <sym|asym> says whether a per-group zero point is kept, <group>
// is the group size, and the _all suffix sends Q4_K down the same path (without it only
// Q6_K/Q5_K are touched):
// q4_sym128 Q6_K/Q5_K -> Q4_0_128 (u4, group 128, symmetric)
// q4_sym128_all and Q4_K too -- drops Q4_K's per-32 zero point, which costs some accuracy
// q4_asym64_all Q6_K/Q5_K and Q4_K -> Q4_1_64 (u4, group 64, asymmetric) -- most of the
// metadata saving while keeping a real zero point
// native no requantization at all (keep Q6_K/Q5_K as they are)
//
// The asymmetric target is only offered in its _all form: leaving Q4_K at its native group 32
// while Q6_K/Q5_K move to group 64 gives the Q/K/V projections different group counts, and the
// GPU plugin's FullyConnectedHorizontalFusion concatenates their scale constants, which then
// fails shape inference. Requantizing all three keeps the group size uniform.
const char * rq = ggml_openvino_getenv_str("GGML_OPENVINO_REQUANT_KQUANT");
auto is_opt = [rq](const char * name) {
return rq && strcmp(rq, name) == 0;
};
const bool sym128 = is_opt("q4_sym128");
const bool sym128_all = is_opt("q4_sym128_all");
const bool asym64_all = is_opt("q4_asym64_all");

if (tensor->type == GGML_TYPE_Q4_K) {
if (sym128_all) {
return ExtraQuantType::Q4_0_128;
}
if (asym64_all) {
return ExtraQuantType::Q4_1_64;
}
}
// MoE expert weights (3D, ne[2] = n_expert) stored as Q5_1/Q8_0 are the expert-side
// equivalent of Q6_K/Q5_K: kept at 8 bits by default while the rest of the model is at 4
// (gemma-4 26B-A4B keeps its down projection there). Send them to 4 bits under the same
// option, at group 64 rather than 128: the down expert has k=704, which 64 divides
// (704/64 = 11) and 128 does not.
if (tensor->ne[2] > 1 && (tensor->type == GGML_TYPE_Q5_1 || tensor->type == GGML_TYPE_Q8_0)) {
if (sym128 || sym128_all) {
return ExtraQuantType::Q4_0_64;
}
if (asym64_all) {
return ExtraQuantType::Q4_1_64;
}
}
switch (tensor->type) {
case GGML_TYPE_Q6_K:
case GGML_TYPE_Q5_K:
if (sym128 || sym128_all) {
return ExtraQuantType::Q4_0_128;
}
if (asym64_all) {
return ExtraQuantType::Q4_1_64;
}
if (is_opt("native")) {
return std::nullopt;
}
return ExtraQuantType::Q8_0_C;
default:
return std::nullopt;
Expand Down Expand Up @@ -321,6 +379,16 @@ ggml_openvino_extracted_layout ggml_openvino_get_extracted_layout(const ggml_ten
layout.weights_per_block = 128;
layout.is_symmetric = true;
break;
case ExtraQuantType::Q4_1_64:
layout.is_u4 = true;
layout.weights_per_block = 64;
layout.is_symmetric = false;
break;
case ExtraQuantType::Q4_0_64:
layout.is_u4 = true;
layout.weights_per_block = 64;
layout.is_symmetric = true;
break;
case ExtraQuantType::Q4_0_C:
layout.is_u4 = true;
layout.weights_per_block = tensor->ne[0];
Expand Down
5 changes: 4 additions & 1 deletion ggml/src/ggml-openvino/ggml-openvino-extra.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@
#include <string>

// ExtraQuantType enum - defines requantization target formats
enum class ExtraQuantType { F16, Q4_0_C, Q8_1_C, Q4_0_128, Q8_0_C, Q8_0_32 };
// Q4_1_64: u4, group 64, *true* asymmetric (per-group scale and zero point). Note that
// Q4_0_128/Q4_0_C are symmetric despite taking the unsigned branch of quantize_q4_0 -- that branch
// pins zp to 8 with d = max/-8, which is algebraically symmetric.
enum class ExtraQuantType { F16, Q4_0_C, Q8_1_C, Q4_0_128, Q4_0_64, Q8_0_C, Q8_0_32, Q4_1_64 };

ov::Core & ov_singleton_core();

Expand Down
8 changes: 3 additions & 5 deletions ggml/src/ggml-openvino/ggml-openvino.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1212,13 +1212,11 @@ static bool is_op_unsupported_case(const ggml_tensor * op) {
if (op->src[0] != nullptr && op->src[0]->ne[2] <= 1) {
return true;
}
if (ggml_openvino_get_device_name() == "GPU" && op->src[0] != nullptr && op->src[0]->type == GGML_TYPE_BF16) {
if (ggml_openvino_get_device_name() == "GPU" && op->src[0] != nullptr && !ggml_is_quantized(op->src[0]->type)) {
return true;
}
// GPU MUL_MAT_ID uses a Gather+MatMul fallback because the GPU plugin rejects internal
// GatherMatmul for these test shapes. Skip cases that would materialize a large selected
// expert-weight temporary.
if (ggml_openvino_get_device_name() == "GPU" && mul_mat_id_requires_large_tmp(op)) {
if (ggml_openvino_get_device_name() == "GPU" && op->src[0] != nullptr && op->src[0]->type == GGML_TYPE_MXFP4 &&
mul_mat_id_requires_large_tmp(op)) {
return true;
}
break;
Expand Down
72 changes: 70 additions & 2 deletions ggml/src/ggml-openvino/ggml-quants.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -851,7 +851,8 @@ std::shared_ptr<ov::Node> requantize_to_buffers(const ggml_tensor * tensor,
const auto * type_traits = ggml_get_type_traits(tensor->type);
const size_t src_row_bytes = ggml_row_size(tensor->type, ne0);

bool is_u4 = (requant_type == ExtraQuantType::Q4_0_C || requant_type == ExtraQuantType::Q4_0_128);
bool is_u4 = (requant_type == ExtraQuantType::Q4_0_C || requant_type == ExtraQuantType::Q4_0_128 ||
requant_type == ExtraQuantType::Q4_0_64 || requant_type == ExtraQuantType::Q4_1_64);

// Streaming dequant (opt-in via GGML_OPENVINO_REDUCE_COMPILE_MEM or
// GGML_OPENVINO_MEMORY_OPTIMIZE): instead of
Expand Down Expand Up @@ -879,7 +880,9 @@ std::shared_ptr<ov::Node> requantize_to_buffers(const ggml_tensor * tensor,
result->set_friendly_name(tensor->name);
return result;
}
if (is_u4) {
if (requant_type == ExtraQuantType::Q4_1_64) {
quantize_q4_1_asym(weights_f32.data(), weights, scales, zp, n_elements, block_size);
} else if (is_u4) {
quantize_q4_0(weights_f32.data(), weights, scales, zp, n_elements, block_size);
} else if (requant_type == ExtraQuantType::Q8_1_C) {
quantize_q8_1(weights_f32.data(), weights, scales, zp, n_elements, block_size);
Expand Down Expand Up @@ -1178,6 +1181,71 @@ void quantize_q4_0(const float * x,
}
}

// Asymmetric u4 quantization with a per-group scale and zero point.
//
// Unlike quantize_q4_0's unsigned branch, which pins the zero point to 8 and is therefore
// symmetric, this keeps a real per-group zero point, so a group whose values are not centred on
// zero does not waste half its range.
void quantize_q4_1_asym(const float * x,
ov::Tensor & weights_arr,
ov::Tensor & scales_arr,
ov::Tensor & zp_arr,
int64_t k,
int64_t qk) {
assert(k % qk == 0);
const int nb = k / qk;

auto * weights = static_cast<uint8_t *>(weights_arr.data());
auto * scales = scales_arr.data<ov::element_type_traits<ov::element::f16>::value_type>();
auto * zp = static_cast<uint8_t *>(zp_arr.data());

// u4 zero points are packed two per byte, low nibble first, indexed by group -- the same
// convention as the unsigned branch of quantize_q4_0.
auto store_zp = [zp](int i, uint8_t v) {
if (i % 2 == 0) {
zp[i / 2] = v & 0x0F;
} else {
zp[i / 2] |= (uint8_t) ((v & 0x0F) << 4);
}
};

for (int i = 0; i < nb; i++) {
float vmin = x[i * qk];
float vmax = x[i * qk];
for (int j = 1; j < qk; j++) {
const float v = x[i * qk + j];
vmin = std::min(vmin, v);
vmax = std::max(vmax, v);
}
// Include 0 in the range so an all-positive or all-negative group still represents zero
// exactly -- these are weights, so an exact zero matters.
vmin = std::min(vmin, 0.0f);
vmax = std::max(vmax, 0.0f);

const float d = (vmax - vmin) / 15.0f;
if (d == 0.0f) {
scales[i] = ov::float16(1.0f);
store_zp(i, 0);
memset(weights + i * qk / 2, 0, qk / 2);
continue;
}
const float id = 1.0f / d;

// The zero point is itself a 4-bit integer, so round it and dequantize as (q - zq) * d.
const int zq = std::max(0, std::min(15, (int) lroundf(-vmin * id)));
scales[i] = ov::float16(d);
store_zp(i, (uint8_t) zq);

for (int j = 0; j < qk / 2; ++j) {
const float x0 = x[i * qk + 2 * j] * id;
const float x1 = x[i * qk + 2 * j + 1] * id;
const uint8_t q0 = (uint8_t) std::max(0, std::min(15, (int) lroundf(x0) + zq));
const uint8_t q1 = (uint8_t) std::max(0, std::min(15, (int) lroundf(x1) + zq));
weights[i * qk / 2 + j] = (uint8_t) (q0 | (q1 << 4));
}
}
}

void quantize_q8_0(const float * x,
ov::Tensor & weights_arr,
ov::Tensor & scales_arr,
Expand Down
10 changes: 10 additions & 0 deletions ggml/src/ggml-openvino/ggml-quants.h
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,10 @@ inline const char * extra_quant_type_name(ExtraQuantType t) {
return "Q8_0_32";
case ExtraQuantType::Q8_1_C:
return "Q8_1_C";
case ExtraQuantType::Q4_0_64:
return "Q4_0_64";
case ExtraQuantType::Q4_1_64:
return "Q4_1_64";
default:
return "unknown";
}
Expand Down Expand Up @@ -166,6 +170,12 @@ void quantize_q8_1(const float * x,
int64_t k,
int64_t qk,
int64_t block_offset = 0);
void quantize_q4_1_asym(const float * x,
ov::Tensor & weights_arr,
ov::Tensor & scales_arr,
ov::Tensor & zp_arr,
int64_t k,
int64_t qk);
void quantize_q8_0(const float * x,
ov::Tensor & weights_arr,
ov::Tensor & scales_arr,
Expand Down
90 changes: 90 additions & 0 deletions ggml/src/ggml-openvino/openvino/op/moe_compressed.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// Copyright (C) 2018-2026 Intel Corporation
// SPDX-License-Identifier: Apache-2.0
//
// Local mirror of OpenVINO's internal ov::op::internal::MOE and MOECompressed ops.
//
// The class bodies are provided by the linked libopenvino.so; only the declarations are
// needed here so the backend can construct the node directly (same approach as
// GatherMatmul and GatedDeltaNet). The class layout must stay in sync with
// openvino/src/core/dev_api/openvino/op/moe.hpp
// openvino/src/common/transformations/include/ov_ops/moe_compressed.hpp
//
// \note MOE op classes are under development and subject to change.

#pragma once

#include <optional>

#include "openvino/core/type/element_type.hpp"
#include "openvino/op/op.hpp"

namespace ov::op::internal {

class OPENVINO_API MOE : public ov::op::Op {
public:
OPENVINO_OP("MOE")

MOE() = default;

MOE(const OutputVector & args) : Op(args) {}

enum class Expert_type { GEMM2_BIAS_SWIGLU_CLAMP, GEMM3_SWIGLU };

enum class Activation_type { SWIGLU, GEGLU_TANH, GEGLU_ERF };

struct Config {
Expert_type expert_type{ Expert_type::GEMM2_BIAS_SWIGLU_CLAMP };
float expert_alpha{ 0.0f };
float expert_beta{ 1.0f };
size_t gate_idx{ 0 };
Activation_type activation_type{ Activation_type::SWIGLU };
};

MOE(const OutputVector & args, const Config & config);

const Config & get_config() const;
void set_config(const Config & config);

bool visit_attributes(AttributeVisitor & visitor) override;
void validate_and_infer_types() override;
std::shared_ptr<Node> clone_with_new_inputs(const OutputVector & new_args) const override;

private:
Config m_config;
};

class OPENVINO_API MOECompressed : public MOE {
public:
OPENVINO_OP("MOECompressed", "", ov::op::internal::MOE)

MOECompressed() = default;

struct Config : public MOE::Config {
size_t hidden_size = 0;
size_t inter_size = 0;
size_t num_expert = 0;
size_t num_shared_expert = 0;
size_t top_k = 0;
// numeric_limits<size_t>::max() means per_channel compression (single group)
size_t group_size = 0;
bool has_batch_dim = false;
bool has_zp = false;
ov::element::Type out_type = ov::element::dynamic;
std::optional<float> scale_factor;
};

MOECompressed(const OutputVector & args, const Config & config);

const Config & get_config() const { return m_config; }

void set_scale_factor(float scale_factor) { m_config.scale_factor = scale_factor; }

bool visit_attributes(AttributeVisitor & visitor) override;
void validate_and_infer_types() override;
std::shared_ptr<Node> clone_with_new_inputs(const OutputVector & new_args) const override;

protected:
Config m_config;
};

} // namespace ov::op::internal
Loading
Loading