Skip to content

[NPUW] Enable vocabulary sharing for the asymmetric vocabulary case - #37054

Open
AsyaPronina wants to merge 9 commits into
openvinotoolkit:masterfrom
AsyaPronina:save_not_all_constants
Open

[NPUW] Enable vocabulary sharing for the asymmetric vocabulary case#37054
AsyaPronina wants to merge 9 commits into
openvinotoolkit:masterfrom
AsyaPronina:save_not_all_constants

Conversation

@AsyaPronina

@AsyaPronina AsyaPronina commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Details:

  • Vocab as input with just saving not all constants

Tickets:

AI Assistance:

  • AI assistance used: no / yes
  • If yes, summarize how AI was used and what human validation was performed (build/tests/manual checks).

@AsyaPronina
AsyaPronina requested review from a team as code owners July 24, 2026 09:39
@github-actions github-actions Bot added category: NPU OpenVINO NPU plugin category: NPUW NPUW plugin labels Jul 24, 2026
@AsyaPronina
AsyaPronina marked this pull request as draft July 24, 2026 09:40
@AsyaPronina
AsyaPronina force-pushed the save_not_all_constants branch from baf31da to 4112c1e Compare August 3, 2026 18:29
@github-actions github-actions Bot added the category: build OpenVINO cmake script / infra label Aug 5, 2026

Copilot AI 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.

Pull request overview

Adds NPUW support for sharing asymmetric quantized vocabularies as inputs, including deferred u8-to-i8 shifting and LM-head rewrites.

Changes:

  • Adds i8 asymmetric unpacking and Sub128 lazy transformation.
  • Adds configurable vocabulary and MatMul-first graph rewrites.
  • Extends partitioning to preserve and process i8 vocabulary constants.

Key risks include an SIMD buffer overrun, ignored configuration, stale tests, and missing regression coverage.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/plugins/intel_npu/src/plugin/npuw/util.cpp Routes i8 asymmetric unpacking.
src/plugins/intel_npu/src/plugin/npuw/util_xarch.hpp Declares the i8 unpack kernel.
src/plugins/intel_npu/src/plugin/npuw/util_xarch.cpp Implements AVX2 i8 dequantization.
src/plugins/intel_npu/src/plugin/npuw/partitioning/patterns/opt.cpp Extends patterns to i8 weights.
src/plugins/intel_npu/src/plugin/npuw/partitioning/partitioning.cpp Applies deferred shifts to closures.
src/plugins/intel_npu/src/plugin/npuw/llm_compiled_model.cpp Adds vocabulary graph rewrites and configuration.
src/plugins/intel_npu/src/plugin/npuw/lazy_tensor.hpp Defines the Sub128 lazy operation.
src/plugins/intel_npu/src/plugin/npuw/lazy_tensor.cpp Implements and serializes Sub128.
src/plugins/intel_npu/src/plugin/CMakeLists.txt Registers the new cross-compiled kernel.
src/plugins/intel_npu/src/al/include/intel_npu/config/npuw_option_defs.inc Defines new NPUW LLM options.

Comment on lines +1505 to +1506
NPUW_ASSERT(from->get_size() == to->get_size());
NPUW_ASSERT(from->get_size() % 8 == 0);

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.

maybe?

Comment on lines +1399 to +1401
auto vocab_as_input = get_option<bool>(other_props, std::string("NPUW_ASYM_VOCAB_AS_INPUT"));
if (m_cfg.get<::intel_npu::NPUW_LLM_ASYM_I8_VOCAB_AS_INPUT>() ||
m_cfg.get<::intel_npu::NPUW_LLM_ASYM_VOCAB_AS_INPUT>()) {

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.

That's valid comment

Comment on lines +1981 to +1983
if ((ov::element::u8 == matched_qweight->get_element_type() ||
ov::element::i8 == matched_qweight->get_element_type()) &&
(standard_layout || pretransposed_layout)) {

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 take a look

Comment on lines +609 to +610
case TransformType::SUB128:
m_transform.emplace<op::Sub128>(ov::npuw::orc::load_versioned_payload<op::Sub128>(section));
Comment on lines +66 to +69
class ConvertVocabAsymU8ToI8 : public ov::pass::MatcherPass {
public:
OPENVINO_MATCHER_PASS_RTTI("ov::npuw::ConvertVocabU8ToI8");
explicit ConvertVocabAsymU8ToI8() {

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.

Do we need tests here?

@dmatveev dmatveev added this to the 2026.4 milestone Aug 11, 2026

@dmatveev dmatveev 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.

Do we need a new HOST_GATHER pattern to properly handle the 0th subgraph?

Comment on lines +93 to +95
INTEL_NPU_NPUW_SIMPLE_OPT(NPUW_LLM_ASYM_VOCAB_AS_INPUT, bool, false, ov::intel_npu::npuw::llm, asym_vocab_as_input, "NPUW_LLM_ASYM_VOCAB_AS_INPUT", LLM, EXPOSED, CACHED, ALL)
INTEL_NPU_NPUW_SIMPLE_OPT(NPUW_LLM_ASYM_I8_VOCAB_AS_INPUT, bool, false, ov::intel_npu::npuw::llm, asym_i8_vocab_as_input, "NPUW_LLM_ASYM_I8_VOCAB_AS_INPUT", LLM, EXPOSED, CACHED, ALL)
INTEL_NPU_NPUW_SIMPLE_OPT(NPUW_LLM_MATMUL_FIRST_VOCAB, bool, false, ov::intel_npu::npuw::llm, matmul_first_vocab, "NPUW_LLM_MATMUL_FIRST_VOCAB", LLM, EXPOSED, CACHED, ALL)

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.

Do we need all these options? I don't think so.

If there was found a way to share asym (u4) vocabs, let just do it by default

Comment on lines +1830 to +1837
} else if (ov::op::util::is_constant(input_node) &&
input_node->get_rt_info().count(ov::npuw::weights::op::Sub128::rt_key) > 0) {
// A Sub128-marked Constant landed in consts_to_keep and stays inline
// in the function body - the shift would be silently skipped there,
// producing wrong numerics. Fail loudly instead.
OPENVINO_THROW("NPUW: Sub128-marked Constant ",
input_node->get_friendly_name(),
" is kept in the function body and won't be transformed");

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.

It should be kept anyway, isn't it?

<< "] (via prototype " << proto_layer_name << ")");
funcall._lazy_closure[param_idx - function._param_offset] =
LazyTensor(std::static_pointer_cast<ov::op::v0::Constant>(input_node)); // (t)/1/c
funcall._lazy_closure[param_idx - function._param_offset] = put_to_closure(input_node); // (t)/1/c

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.

Not sure why this special handling is required here.

Comment on lines -60 to 66
ov::Tensor Const::eval() const {
ov::Tensor Const::eval_view() const {
if (m_node) {
return ov::npuw::util::copy_tensor_from_const(m_node);
return ov::npuw::util::tensor_from_const(m_node);
}

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.

???

Comment on lines +381 to +405
ov::Tensor Sub128::eval() const {
const auto trs = tensor.get_transformations();

ov::Tensor src;
if (trs.size() == 1 && std::holds_alternative<op::Const>(trs.front())) {
// Fused path: read straight through a zero-copy view of the source,
// skipping the intermediate copy Const::eval() would make. The view is
// only ever READ here, so this is correct for all Const flavors,
// including the deserialized ones (read-only mmap / cached bin tensor)
src = std::get<op::Const>(trs.front()).eval_view();
} else {
src = tensor.eval();
}

const auto src_type = src.get_element_type();
NPUW_ASSERT(src_type == ov::element::u8 || src_type == ov::element::i8);

ov::Tensor dst(ov::element::i8, src.get_shape());
const auto* s = static_cast<const uint8_t*>(src.data());
auto* d = dst.data<int8_t>();
for (std::size_t i = 0, n = src.get_size(); i < n; ++i) {
d[i] = static_cast<int8_t>(static_cast<int8_t>(s[i]) - 128);
}
return dst;
}

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.

Honestly this could've been a generic subtract, I see no point in leaving it 128 only

Comment on lines +66 to +67
class ConvertVocabAsymU8ToI8 : public ov::pass::MatcherPass {
public:

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.

I believe this pass must be moved to the passes.

auto vocab_as_input = get_option<bool>(other_props, std::string("NPUW_ASYM_VOCAB_AS_INPUT"));
if (m_cfg.get<::intel_npu::NPUW_LLM_ASYM_I8_VOCAB_AS_INPUT>() ||
m_cfg.get<::intel_npu::NPUW_LLM_ASYM_VOCAB_AS_INPUT>()) {
lm_head_config["NPUW_HOST_GATHER"] = "NO";

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.

Why? I believe HOST_GATHER was never a case for the lmhead submodel?

Comment on lines +1756 to +1764
ov::npuw::weights::LazyTensor put_to_closure(const std::shared_ptr<ov::Node>& input_node) {
auto const_node = std::static_pointer_cast<ov::op::v0::Constant>(input_node);
ov::npuw::weights::LazyTensor lt(const_node);
if (const_node->get_rt_info().count(ov::npuw::weights::op::Sub128::rt_key) > 0) {
LOG_DEBUG("Sub128 marker found on " << const_node->get_friendly_name() << " - applying to the LazyTensor");
lt = lt.sub128();
}
return lt;
}

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.

This sound very strange to me. Why we should check it here? Why cant it be part of the normal evaluation?

Comment on lines +1399 to +1401
auto vocab_as_input = get_option<bool>(other_props, std::string("NPUW_ASYM_VOCAB_AS_INPUT"));
if (m_cfg.get<::intel_npu::NPUW_LLM_ASYM_I8_VOCAB_AS_INPUT>() ||
m_cfg.get<::intel_npu::NPUW_LLM_ASYM_VOCAB_AS_INPUT>()) {

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.

We don't need these options I believe

@dmatveev dmatveev changed the title [NPUW] Share 1 vocab [NPUW] Enable vocabulary sharing for the asymmetric vocabulary case Aug 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

category: build OpenVINO cmake script / infra category: NPU OpenVINO NPU plugin category: NPUW NPUW plugin

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants