Skip to content

[IE] Defense-in-depth: reject zero-dim FC ops in UnrollFullyConnected#266

Closed
blairducrayoppat wants to merge 4 commits into
openvinotoolkit:developfrom
blairducrayoppat:fix/unroll-fc-zero-dim-guard
Closed

[IE] Defense-in-depth: reject zero-dim FC ops in UnrollFullyConnected#266
blairducrayoppat wants to merge 4 commits into
openvinotoolkit:developfrom
blairducrayoppat:fix/unroll-fc-zero-dim-guard

Conversation

@blairducrayoppat

@blairducrayoppat blairducrayoppat commented Mar 4, 2026

Copy link
Copy Markdown

[IE] Defense-in-depth: reject zero-dim FC ops in UnrollFullyConnected

Summary

UnrollFullyConnected::matchAndRewrite() does not guard against zero or negative dimensions in IE::FullyConnectedOp operands before entering the splitLeftInput / reshapeTo2d unrolling logic. When per-group INT4 quantization decomposition (e.g. GroupWisePatternRewriter with group_size=128) produces intermediate FC ops with degenerate shapes, the unrolling pass propagates zero-dim tensors into unrolled sub-FCs. These poisoned sub-FCs then reach downstream passes like ConvertFCToConv, triggering a process-killing SIGABRT in IE::ConvolutionOp type inference.

This PR adds a defense-in-depth guard in matchAndRewrite() that rejects FC ops with zero or negative batch dimension or weight dimensions. The existing inputChannels < numChunks guard already catches some zero-dim edge cases on the LHS channel axis; this addition covers the batch dimension and all weight dimensions.

Context

This is a companion to the ConvertFCToConv fix (#265). That fix prevents the crash at the ConvertFCToConv layer; this fix prevents degenerate shapes from propagating through UnrollFullyConnected in the first place.

The ConvertFCToConv fix is the primary crash prevention mechanism. This UnrollFullyConnected guard is strictly defense-in-depth — it hardens an earlier pass in the pipeline to reject shapes that should never have reached it.

Root Cause

The location trail from the original crash (["fc_decomposed", "matmul_0", "as_convolution"]) shows that UnrollFullyConnected runs between GroupWisePatternRewriter and ConvertFCToConv. When the decomposition produces a zero-dim FC, UnrollFullyConnected blindly enters splitLeftInput, which slices the LHS tensor into numChunks equal blocks. If the batch dimension is zero, the sliced sub-FCs inherit the zero-dim shape and propagate it downstream.

Changes

src/vpux_compiler/src/dialect/IE/transforms/passes/unroll_fully_connected.cpp:

  • Added +18 lines after the existing inputChannels < numChunks guard (line ~475).
  • Batch dimension check: lhsShape[Dim(0)] <= 0 → return mlir::failure() with debug log.
  • Weight dimensions check: Iterates all weight dimensions via irange(wShape.size()); any dimension <= 0 → return mlir::failure() with debug log.
  • Block-scoped wShape variable to avoid shadowing outer variables.
  • References openvinotoolkit/openvino#34450 in comment.

Testing

  • LIT test added: tests/lit/NPU/dialect/IE/passes/unroll_fully_connected_zero_dim_guard.mlir — constructs a FakeQuantize→Concat→AffineReshape→Transpose→FullyConnected pattern with a zero batch dimension (tensor<0x3072xf32>). Verifies the guard catches lhsShape[Dim(0)] <= 0 and preserves the FC unchanged.
  • Existing unroll_fully_connected.mlir regression test passes on all three architectures (NPU37XX, NPU40XX, NPU50XX) — no behavioral change for valid-shaped FC ops.

AI Usage

-This PR was developed with AI assistance (GitHub Copilot) for source code analysis, fix implementation, and PR description drafting. All code was reviewed and validated by the contributor.

Related

Add a guard in UnrollFullyConnected::matchAndRewrite() that rejects
FullyConnectedOp operands with zero or negative dimensions before
proceeding with the group-quantized unrolling split.

This prevents degenerate shapes produced by multi-pass quantization
decomposition (GroupWisePatternRewriter + UnrollFullyConnected
interaction) from propagating into unrolled sub-FCs, which would
otherwise reach downstream passes like ConvertFCToConv and trigger
a SIGABRT in ConvolutionOp type-inference.

The existing inputChannels guard already catches zero-dim LHS inputs;
this addition covers the batch dimension and all weight dimensions.

Related: openvinotoolkit/openvino#34450
Defense-in-depth guard for degenerate zero-dim shapes in splitLeftInput.
Companion to openvinotoolkit/openvino#34450.
See PR_DESCRIPTION.md on fix/convert-fc-to-conv-zero-dim-guard for primary fix.
@blairducrayoppat blairducrayoppat force-pushed the fix/unroll-fc-zero-dim-guard branch from cb07b09 to 6f4eb62 Compare April 16, 2026 21:37
@blairducrayoppat

Copy link
Copy Markdown
Author

Rebased against current develop (merge commit removed — clean linear history). Companion defense-in-depth for #265. Minimal change (+54 lines).

@DariaMityagina

DariaMityagina commented Apr 17, 2026

Copy link
Copy Markdown
Contributor

@blairducrayoppat hi! Could you please cover your changes with a LIT test?

@andrey-golubev andrey-golubev left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hi, thanks for you contribution! I believe the root cause here stems from the fact that an operation with zero-dim tensor exists at all. This should be prohibited by NPU compiler.

Does it come from OpenVINO directly? I'd say then we have to talk with OpenVINO to decide how this should be handled, because right now I would suggest to just reject such a model.

Alternatively, if zero-dim tensor comes from another compiler pass, this is where it has to be fixed.

Verifies that FullyConnected ops with a zero batch dimension
(tensor<0x3072xf32>) are preserved unchanged by the pass.

The test constructs a FakeQuantize->Concat->AffineReshape->Transpose->FC
pattern where inputChannels (3072) passes the existing divisibility check
but lhsShape[Dim(0)] == 0 triggers the new defense-in-depth guard.

See openvinotoolkit/openvino#34450.
@blairducrayoppat

Copy link
Copy Markdown
Author

Thank you both for the thorough review.

LIT test added

@DariaMityagina — A LIT test has been added in this push:

tests/lit/NPU/dialect/IE/passes/unroll_fully_connected_zero_dim_guard.mlir

The test constructs the same FakeQuantize → Concat → AffineReshape → Transpose → FullyConnected pattern used by the existing @UnrollMatMul tests, but with a zero batch dimension (tensor<0x3072xf32>). The inputChannels (3072) is evenly divisible by numChunks (3), so it passes the existing divisibility check at line 474. The new guard catches lhsShape[Dim(0)] <= 0 and returns mlir::failure(), preserving the entire IR subgraph unchanged.

I verified empirically (against the unpatched upstream/develop binary) that without this guard, the pass unrolls the zero-batch FC into 3 degenerate sub-FCs:

IE.Slice %arg0 [0, 0] [0, 1024] : tensor<0x3072xf32> to tensor<0x1024xf32>
IE.FullyConnected(%6, %9) : tensor<0x1024xf32>, tensor<4096x1024xf32> -> tensor<0x4096xf32>

Three such sub-FCs are created and accumulated via IE.Add, propagating the degenerate shape downstream.

Root cause investigation — what I know vs. what I don't

@andrey-golubev — I accept the architectural direction: fixing the source that produces zero-dim shapes is the correct long-term solution, and this guard is defense-in-depth.

What I can confirm:

  1. The crash location trail from the original bug is fc_decomposed → matmul_0 → as_convolution, meaning GroupWisePatternRewriter (in InitialLowPrecisionTransformations) created the FC, then UnrollFullyConnected processed it, then ConvertFCToConv crashed.

  2. The pipeline order (verified from NPU40XX/pipelines.cpp): InitialLowPrecisionTransformations runs early → dozens of intermediate passes → UnrollGroupQuantizeUnrollFullyConnectedConvertFCToConv.

  3. GroupWisePatternRewriter itself (in decompose_multi_zp_quantization_pattern.cpp) has a VPUX_THROW_UNLESS at line 218 that would crash (not produce zero-dim) on invalid inputs at its own execution time. This means the zero-dim is likely not present when GroupWisePatternRewriter runs, but is introduced by one of the dozens of intermediate passes between InitialLowPrecisionTransformations and UnrollFullyConnected.

What I cannot determine externally:

I don't have access to an IR dump from a full Qwen3-0.6B INT4 compilation on NPU40XX. Without intermediate IR snapshots, I cannot identify which specific pass between InitialLowPrecisionTransformations and UnrollFullyConnected introduces the zero-dim shape.

Request: Could you point me to the recommended way to dump IR between passes in the NPU pipeline? Something like --mlir-print-ir-after-all or a pipeline-specific flag? With that, I can compile Qwen3-0.6B INT4, capture the intermediate IR, identify the pass that introduces the zero-dim, and file a targeted fix at the source.

In the meantime, this guard prevents the degenerate shapes from propagating into unrolled sub-FCs and eventually crashing in ConvertFCToConv.

@blairducrayoppat

blairducrayoppat commented May 12, 2026

Copy link
Copy Markdown
Author

@andrey-golubev — when you have a moment, would you have guidance on the IR dumping methodology? Specifically: which env vars or compiler flags let me capture the IR after each pass in the NPU pipeline, so I can identify where the zero-dim shape is introduced? I want to follow your direction on fixing this at the root cause but haven't been able to find the right knob from outside the project.
Happy to wait as I am sure there are greater priorities for the team — just want to make sure you have what you need from me to keep this moving.

@DariaMityagina — also flagging: I added the LIT test you requested at tests/lit/NPU/dialect/IE/passes/unroll_fully_connected_zero_dim_guard.mlir in commit 9c7526f. Let me know if it's structured the way you'd want.

@blairducrayoppat

Copy link
Copy Markdown
Author

@andrey-golubev — following your guidance here ("if zero-dim tensor comes from another compiler pass, this is where it has to be fixed"), I rebuilt current develop (30d2bb87b) with assertions and traced what happens now.

The zero-dim as_convolution crash this PR guards no longer reproduces on develop for the Qwen3-0.6B grouped-INT4 NPUW path — there is no tensor<0x…> anywhere in the pass dump. The failure has shifted to a location-verifier abort:

StopLocationVerifierPass Pass failed : Found 40 duplicated names after full verification

Root cause (from the verifier's own duplicate report): UnrollGroupQuantize names each unrolled slice after its input value (appendLoc(val.getLoc(), "slice_{0}", idx)). In each attention projection one per-group scale Parameter is the dequant scale of two IE.DynamicDequantize ops (weight dequant + matmul dequant); both are unrolled, so the shared scale's low-index slices get identical locations — the 40 duplicates the verifier reports. That is a pass emitting non-unique locations, i.e. the "fix where it appears" case you described.

I have a one-file fix in UnrollGroupQuantize that locates each slice after its consuming op + an operand tag (the takeOpLoc convention ConvertGroupConvToConv uses), with a LIT regression test. With it the verifier passes (40 → 0); the model then hits an unrelated sub-byte lowering limit, so this is purely the location-uniqueness fix, not a support-enabling change. Shall I open it as a separate PR against develop and link it here? And since the zero-dim guard in this PR no longer has a live reproducer on develop, I'm happy to close or repoint #265/#266 — whatever you prefer.


AI assistance disclosure (per the OpenVINO AI Usage Policy): a coding agent (Claude, via Claude Code) helped trace this failure on a local developer build, identify the root cause, draft the fix and its LIT regression test, and draft this comment. Every result quoted above — the duplicate-name report, the 40 → 0 verifier outcome, and the red-on-develop / green-with-fix test runs — was produced by building and running the compiler locally on Intel NPU (Lunar Lake) and was reviewed by me before posting.

@blairducrayoppat

Copy link
Copy Markdown
Author

@andrey-golubev @DariaMityagina — superseding this. As noted on #265, the zero-dim symptom no longer reproduces on current develop; the real issue was non-unique slice locations emitted by UnrollGroupQuantize, now fixed at the source in #302 (with a LIT test). Closing in favor of the root-cause fix rather than keeping a defense-in-depth guard. Thanks both for the review.

@DariaMityagina

Copy link
Copy Markdown
Contributor

@andrey-golubev @DariaMityagina — superseding this. As noted on #265, the zero-dim symptom no longer reproduces on current develop; the real issue was non-unique slice locations emitted by UnrollGroupQuantize, now fixed at the source in #302 (with a LIT test). Closing in favor of the root-cause fix rather than keeping a defense-in-depth guard. Thanks both for the review.

@blairducrayoppat thank you for the update!

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.

3 participants